数据结构之(五):散列表(哈希表)

概念
Hash 散列 哈希 杂凑
把任意长度的输入 通过算法 变换成固定长度的输出
相较于 顺序存储结构而言 当存储量达到一定程度时 查找效率得到提高
空间换时间
映射关系,根据关键字 key 访问到具体值 value
不同 key 映射到同一个地址 哈希碰撞 或 哈希冲突

哈希函数

1 )直接寻址法
取关键字或关键字的线性函数 作为散列地址
2 )除留取余法
对关键字或关键字的部分取模 作为散列地址 取模的除数 一般为素数 / 质数
取模的除数 一般为素数 / 质数
3 )取随机数法
使用随机函数,取关键字的随机值 作为散列地址
4 )数字分析法
根据数字的特性,经过分析,取部分进行计算(如手机号后四位 身份证后四位等等)
5 )平方取中法
先求平方,取中间几位 作为散列地址
6 )折叠法
取关键字的几部分 取叠加和 作为散列地址
发生哈希冲突的原因 —— 抽屉原理
解决冲突的办法: 再找一个空闲位置
具体如下
1 )线性探测
key 01 —— 1 号柜子
如果满了 顺延到下一个位置 —— 2 号柜子
2 )二次探测
如果满了 按照一定规律顺延
如以二次方顺延 value value+1^2 value+2^2 3 )双重哈希
使用两种哈希函数 第一个位置被占用时 计算第二个
4 )链表法
让一个位置 存储多个 value ( 用链表串联起来 )
哈希应用之两数之和

两数之和
https://leetcode-cn.com/problems/two-sum/
1. 两数之和
给定一个整数数组 nums 和一个目标值 target ,请你在该数组中找出和为目标值的那 两个
整数,并返回他们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素不能使用两遍。
示例 :
给定 nums = [2, 7, 11, 15], target = 4
因为 nums[0] + nums[1] = 2 + 7 = 9
所以返回 [0, 1]
一、暴力破解法
2 [7, 11, 15]
7 [11, 15]
11 [15]
遍历每个元素 查找后续元素与其相加的和 是否等于 target
依次遍历出 每两个元素之和
二、倒推法
使用额外容器存储 快速找到是否存在某个值
< 元素值,索引位置 > hashmap
[2, 7, 11, 15]
<2,0> <7,1> <11,2> <15,3>
26
2 26-2 = 24
7 26-7 = 19
11 26-11 = 15
三、一次哈希法
[2, 7, 11, 15] 4
26 map
2 26-2 = 24 <2,0>
7 26-7 = 19 <2,0> <7,1>
11 26-11 = 15 <2,0> <7,1> <11,2>
15 26-15 = 11
边遍历边修改 map 的值 能达到最好效率

    public static int[] twoSum(int[] nums, int target) {
//遍历每个元素 查找后续元素与其相加的和 是否等于target
        for (int i = 0; i < nums.length; i++) {
            for (int j = i + 1; j < nums.length; j++) {
                if (nums[i] + nums[j] == target) {
                    return new int[]{i, j};
                }
            }
        }
        return new int[]{-1, -1};
    }
    public static int[] twoSum1(int[] nums, int target) {
// 使用map 存储 <元素值,索引位置>
        Map map = new HashMap<>();
        for (int i = 0; i < nums.length; i++) {
            map.put(nums[i], i);
        }
        for (int i = 0; i < nums.length; i++) {
            int needNum = target - nums[i];
// 数组中同一个元素不能使用两遍
            if (map.containsKey(needNum) && map.get(needNum) != i) {
                return new int[]{i, map.get(needNum)};
            }
        }
        return new int[]{-1, -1};
    }
    public static int[] twoSum2(int[] nums, int target) {
// 边遍历边修改map的值 能达到最好效率
        Map map = new HashMap<>();
        for (int i = 0; i < nums.length; i++) {
            int needNum = target - nums[i];
            if (map.containsKey(needNum)) {
                return new int[]{map.get(needNum), i};
            }
            map.put(nums[i], i);
        }
        return new int[]{-1, -1};
    }
哈希应用之找不同
找不同
https://leetcode-cn.com/problems/fifind-the-difffference/
389. 找不同
给定两个字符串 s t ,它们只包含小写字母。
字符串 t 由字符串 s 随机重排,然后在随机位置添加一个字母。
请找出在 t 中被添加的字母。
示例 1
输入: s = "abcd", t = "abcde"
输出: "e"
解释: 'e' 是那个被添加的字母。
示例 2
输入: s = "", t = "y"
输出: "y"
示例 3
输入: s = "a", t = "aa"
输出: "a"
示例 4
输入: s = "ae", t = "aea"
输出: "a
方案一:
使用 map 分别记录 s t < 字母,出现的次数 >
查找 t 中出现次数多了一次 或者在 s 中从未出现的字母
s = "abcd", t = "abcde"
map
找到 e 返回
s = "abcd", t = "abcda"
map
第二次遍历到 a 此时次数为 0 说明出现额外一次字母 a
    public static char findTheDifference(String s, String t) {
        Map map = new HashMap<>();
// 存储s中的字母 及其出现次数
        for (Character c : s.toCharArray()) {
            if (map.containsKey(c)) {
                int newNum = map.get(c) + 1;
                map.put(c, newNum);
                continue;
            }
            map.put(c, 1);
        }
        for (char tc : t.toCharArray()) {
// 在s中从未出现的字母
            if (!map.containsKey(tc)) {
                return tc;
            }
// 查找t中出现次数多了一次
            if (map.get(tc) == 0) {
                return tc;
            }
            int newNum = map.get(tc) - 1;
            map.put(tc, newNum);
        }
        return '-';
    }
方案二:
字符串的替换方法 replace()
遍历 s 中每个字母 将其在 t 中替换为空 t 最后只剩一个字母
    public static char findTheDifference1(String s, String t) {
        for (Character c : s.toCharArray()) {
// 替换第一个出现的位置
            t = t.replaceFirst(c.toString(), "");
        }
        return t.toCharArray()[0];
    }
方案三:
根据 ascii 码表的特性
分别遍历 s t 将每个字母的值相加 所得结果相减 差值就是要找的值
a + b + c + d = 97 + 98 + 99 + 100 = 394
a + b + c + d + e = 97 + 98 + 99 + 100 + 101 = 495
    // 根据ascii码表的特性
    public static char findTheDifference2(String s, String t) {
        int sSum = 0, tSum = 0;
        for (Character c : s.toCharArray()) {
            sSum += c;
        }
        for (Character c : t.toCharArray()) {
            tSum += c;
        }
        return (char) (tSum - sSum);
    }
方案四:
异或运算 二进制运算
0 ^ 1 = 1 ^ 0 = 1 两者不同
0 ^ 0 = 1 ^ 1 = 0 两者相同
a ^ b ^ c ^ a ^ b = (a ^ a) ^ (b ^ b) ^ c = c
s = "abcd", t = "abcde"
(a ^ b ^ c ^ d) ^ (a ^ b ^ c ^ d ^ e) = e
    //异或运算
    public static char findTheDifference3(String s, String t) {
        int result = 0;
        for (Character c : s.toCharArray()) {
            result ^= c;
        }
        for (Character c : t.toCharArray()) {
            result ^= c;
        }
        return (char) result;
    }
递归经典应用之斐波那契数列
(一)递归概
将复杂问题 递推分解为最简问题 然后将结果回归的过程
Windows - Linux
Linux = Linux is not Unix
使用方法: 自己调用自己
(二)斐波那契数列

兔子问题
有一对大兔子 每个月繁衍 一对小兔子(一公一母)
小兔子 每个月生长为 大兔子
现有一对小兔子 一年后 有多少对?
M1 1 A
M2 1 A~
M3 2 A->B
M4 3 A->C + B~
M5 5 A->D + B->E + C~
M6 8 A->F + B->G + C->H + D~ + E~
当前的所有兔子 = 上个月的所有兔子 + 这个月新生的兔子(可以繁衍的兔子)
= 上个月的所有兔子数量 + 上上个月的所有兔子数量(经过了一个月的生长周期)
Mn = M(n-1) + M(n-2)
M5 = M4 + M3
  = (M3+M2) + (M2+M1)
  = (M2+M1 + M2) + (M2+M1)
  = 1+1+1 + 1+1 = 5
M1=1 M2=1
使用方式:
1 )推导出递推公式 —— 找规律
2 )找到递推的出口 —— 找出口

数据结构之(五):散列表(哈希表)_第1张图片

    public static int fib(int N) {
        if (N == 1) return 1;
        if (N == 2) return 1;
        System.out.println("求第" + N + "个月的兔子数量");
        System.out.println("转化为求第" + (N - 1) + "个月和第" + (N - 2) + "个月的兔 子数量");
        return fib(N - 1) + fib(N - 2);
    }
大部分递归 可以转化为迭代处理
Make it work,Make it right,Make it fast
思路:使用数组存储,通过 n-1 n-2 的值进行计算
    public static int fib1(int N) {
// 6 —— 0 1 2 3 4 5
// fib(0) = 0 有时需要处理
        if (N <= 1) return 1;
        int[] arr = new int[N];
        arr[0] = 1;
        arr[1] = 1;
        for (int i = 2; i < arr.length; i++) {
            arr[i] = arr[i - 1] + arr[i - 2];
        }
        return arr[N - 1];
    }
递归经典应用之汉诺塔
汉诺塔

印度的恒河 瓦拉那西 诞生婆罗门教
放了三根柱子,其中一个根柱子上放了 64 个圆盘
需要将全部圆盘 移动到另一根柱子上
并且 每次只能移动一个 移动过程中 小圆盘必须在大圆盘之上
为何不可完成?
分析:
一个圆盘 A->C
两个圆盘 A->B A->C B->C
三个圆盘 A->C A->B C->B ( 把前两个圆盘 从 A 移动到 B)
     A->C ( 移动最大的圆盘 )
     B->A B->C A->C ( 再把前两个圆盘 从 B 移动到 C)
N 个圆盘
   先把前 N-1 个圆盘 A 移动到 B ( 经由 C)
   再把最大的圆盘 A 移动到 C
   最后把前 N-1 个圆盘 B 移动到 C ( 经由 A)
移动次数
H(1) = 1
H(2) = 3
H(3) = H(2) + 1 + H(2) = 7
H(4) = 7 + 1 + 7 = 15
H(N) = H(N-1) + 1 + H(N-1) = 2^N - 1
    // 四个参数 有n个圆盘 需要从A柱子移动到C 经由B
// 起始 中间 终点
    public static void hanoi(int n, char A, char B, char C) {
// 出口
        if (n == 1) {
            System.out.println(A + "->" + C);
            return;
        }
// 先把前N-1个圆盘 从A移动到B (经由C)
// 再把最大的圆盘 从A移动到C
// 最后把前N-1个圆盘 从B移动到C (经由A)
        hanoi(n - 1, A, C, B);
        System.out.println(A + "->" + C);
        hanoi(n - 1, B, A, C);
    }

你可能感兴趣的:(数据结构,散列表,哈希算法)