数据结构合集

https://blog.csdn.net/cywosp/article/details/23397179

https://blog.csdn.net/qq_43621789/article/details/106917404

一致性哈希算法

哈希算法好坏的定义

1、平衡性(Balance):平衡性是指哈希的结果能够尽可能分布到所有的缓冲中去,这样可以使得所有的缓冲空间都得到利用。很多哈希算法都能够满足这一条件。

2、单调性(Monotonicity):单调性是指如果已经有一些内容通过哈希分派到了相应的缓冲中,又有新的缓冲加入到系统中。哈希的结果应能够保证原有已分配的内容可以被映射到原有的或者新的缓冲中去,而不会被映射到旧的缓冲集合中的其他缓冲区。 

3、分散性(Spread):在分布式环境中,终端有可能看不到所有的缓冲,而是只能看到其中的一部分。当终端希望通过哈希过程将内容映射到缓冲上时,由于不同终端所见的缓冲范围有可能不同,从而导致哈希的结果不一致,最终的结果是相同的内容被不同的终端映射到不同的缓冲区中。这种情况显然是应该避免的,因为它导致相同内容被存储到不同缓冲中去,降低了系统存储的效率。分散性的定义就是上述情况发生的严重程度。好的哈希算法应能够尽量避免不一致的情况发生,也就是尽量降低分散性。 

4、负载(Load):负载问题实际上是从另一个角度看待分散性问题。既然不同的终端可能将相同的内容映射到不同的缓冲区中,那么对于一个特定的缓冲区而言,也可能被不同的用户映射为不同 的内容。与分散性一样,这种情况也是应当避免的,因此好的哈希算法应能够尽量降低缓冲的负荷。

为什么需要一致性哈希算法?

Hash算法解决的是集群管理中请求访问的路由的问题,一般是根据一个请求的某个key值取余,路由到相应的服务器,比如,Key的hashCode是52,服务器的数目是3,取余之后为1,则该key对应的节点是Node1,由于HashCode随机性比较强,所以使用余数Hash路由算法就可以保证缓存数据在整个缓存服务器集群中有比较均衡的分布,这就是余数Hash的算法,==但是这种算法会有一个问题就是集群扩容了或者服务器下线了,那么取余的方式,原来Key映射到节点可能不会是Node1,可能会导致某个节点的请求压力变大,一致性哈希解决了普通余数Hash算法伸缩性差的问题,可以保证在上线、下线服务器的情况下尽量有多的请求命中原来路由到的服务器。==

环形Hash空间

按照常用的hash算法来将对应的key哈希到一个具有2^32次方个桶的空间中,即0~(2^32)-1的数字空间中。现在我们可以将这些数字头尾相连,想象成一个闭合的环形。如下图

==为什么是2^32个桶空间?==

  • 当hash是一个32位无符号整型时,就是2^32,如果用64位,那就是2^64

把数据通过一定的hash算法处理后映射到环上

现在我们将object1、object2、object3、object4四个对象通过特定的Hash函数计算出对应的key值,然后散列到Hash环上。如下图:

  • Hash(object1) = key1;
  • Hash(object2) = key2;
  • Hash(object3) = key3;
  • Hash(object4) = key4;

将机器通过hash算法映射到环上

在采用一致性哈希算法的分布式集群中将新的机器加入,其原理是通过使用与对象存储一样的Hash算法将机器也映射到环中(一般情况下对机器的hash计算是采用机器的IP或者机器唯一的别名作为输入值),然后以顺时针的方向计算,将所有对象存储到离自己最近的机器中。

假设现在有NODE1,NODE2,NODE3三台机器,通过Hash算法得到对应的KEY值,映射到环中,其示意图如下:

  • Hash(NODE1) = KEY1;
  • Hash(NODE2) = KEY2;
  • Hash(NODE3) = KEY3;

==此图是最理想的情况,三个点正好均匀分布==

通过上图可以看出==对象与机器处于同一哈希空间中==,这样==按顺时针转动object1存储到了NODE1中,object3存储到了NODE2中,object2、object4存储到了NODE3中。==在这样的部署环境中,hash环是不会变更的,因此,通过算出对象的hash值就能快速的定位到对应的机器中,这样就能找到对象真正的存储位置了。

机器的删除与添加

普通hash求余算法最为不妥的地方就是在有机器的添加或者删除之后会照成大量的对象存储位置失效,这样就大大的不满足单调性了。下面来分析一下一致性哈希算法是如何处理的。

1. 节点(机器)的删除

以上面的分布为例,如果NODE2出现故障被删除了,那么按照顺时针迁移的方法,object3将会被迁移到NODE3中,这样仅仅是object3的映射位置发生了变化,其它的对象没有任何的改动。如下图:

==如果使用之前的hash算法,服务器数量发生改变时,所有服务器的所有缓存在同一时间失效了,而使用一致性哈希算法时,服务器的数量如果发生改变,并不是所有缓存都会失效,而是只有部分缓存会失效,前端的缓存仍然能分担整个系统的压力,而不至于所有压力都在同一时间集中到后端服务器上。==

2. 节点(机器)的添加

如果往集群中添加一个新的节点NODE4,通过对应的哈希算法得到KEY4,并映射到环中,如下图:

通过按顺时针迁移的规则,那么object2被迁移到了NODE4中,其它对象还保持这原有的存储位置。通过对节点的添加和删除的分析,一致性哈希算法在保持了单调性的同时,还是数据的迁移达到了最小,这样的算法对分布式集群来说是非常合适的,避免了大量数据迁移,减小了服务器的的压力。

==注意,上面两种情况节点的分布不平均了,会导致某一台服务器的数据量过多==

平衡性

根据上面的图解分析,一致性哈希算法满足了单调性和负载均衡的特性以及一般hash算法的分散性,但这还并不能当做其被广泛应用的原由,因为还缺少了平衡性。下面将分析一致性哈希算法是如何满足平衡性的。==hash算法是不保证平衡的,如上面只部署了NODE1和NODE3的情况(NODE2被删除的图),object1存储到了NODE1中,而object2、object3、object4都存储到了NODE3中,这样就照成了非常不平衡的状态。==在一致性哈希算法中,为了尽可能的满足平衡性,其引入了虚拟节点。

  • ==“虚拟节点”( virtual node )是实际节点(机器)在 hash 空间的复制品( replica ),一实际个节点(机器)对应了若干个“虚拟节点”,这个对应个数也成为“复制个数”,“虚拟节点”在 hash 空间中以hash值排列。==

根据上图可知对象的映射关系:object1->NODE1-1,object2->NODE1-2,object3->NODE3-2,object4->NODE3-1。通过虚拟节点的引入,对象的分布就比较均衡了。

==“虚拟节点”的hash计算可以采用对应节点的IP地址加数字后缀的方式。例如假设NODE1的IP地址为192.168.1.100。引入“虚拟节点”前,计算 cache A 的 hash 值:== Hash(“192.168.1.100”); 引入“虚拟节点”后,计算“虚拟节”点NODE1-1和NODE1-2的hash值: Hash(“192.168.1.100#1”); // NODE1-1 Hash(“192.168.1.100#2”); // NODE1-2

==虚拟节点越多,hash环上的节点就越多,缓存被均匀分布的概率就越大。==

八大排序以及java实现

插入排序

算法描述

一般来说,插入排序都采用in-place在数组上实现。具体算法描述如下:

  1. 从第一个元素开始,该元素可以认为已经被排序
  2. 取出下一个元素,在已经排序的元素序列中从后向前扫描
  3. 如果该元素(已排序)大于新元素,将该元素移到下一位置
  4. 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置
  5. 将新元素插入到该位置后
  6. 重复步骤2~5

img

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public int[] sort(int[] nums) {
  int len = nums.length;
  for(int i = 1; i < len; i++) {
    int n = nums[i];
    int j = i;
    for(; j>0 && n<nums[j-1]; j--) {
			nums[j] = nums[j-1];
    }
    nums[j] = n;
  }
  return nums;
}

直接插入排序复杂度如下:

平均时间复杂度 最好情况 最坏情况 空间复杂度 稳定性
O(n²) O(n²) O(n²) O(1) 稳定

选择排序

算法描述

  1. 从未排序序列中,找到关键字最小的元素
  2. 如果最小元素不是未排序序列的第一个元素,将其和未排序序列第一个元素互换
  3. 重复1、2步,直到排序结束。

img

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public int[] sort(int[] nums) {
    int len = nums.length;
        for(int i = 0; i < len; i++) {
            int min = i;
            for(int j = i + 1; j < len; j++) {
                    if(nums[j] < nums[min]) {
                        min = j;
                    }
            }
            int tmp = nums[i];
            nums[i] = nums[min];
            nums[min] = tmp;
        }
        return nums;
}
平均时间复杂度 最好情况 最坏情况 空间复杂度 稳定性
O(n²) O(n²) O(n²) O(1) 不稳定

冒泡排序

算法描述

冒泡排序算法的运作如下:

  1. 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
  2. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
  3. 针对所有的元素重复以上的步骤,除了最后一个。
  4. 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。

img

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public int[] sortArray(int[] nums) {
        int len = nums.length;
        for(int i = 0; i < len; i++) {
            for(int j = 0; j < len-i-1; j++) {
                    if(nums[j] > nums[j+1]) {
                        int tmp = nums[j];
                        nums[j] = nums[j+1];
                        nums[j+1] = tmp;
                    }
            }
        }
        return nums;
    }

改进的冒泡排序最好情况可以做到O(n),就是遍历了一次发现全部有序,则无需在多几轮了

平均时间复杂度 最好情况 最坏情况 空间复杂度 稳定性
O(n²) O(n) O(n²) O(1) 稳定

快速排序

算法描述

快速排序使用分治策略来把一个序列(list)分为两个子序列(sub-lists)。步骤为:

  1. 从数列中挑出一个元素,称为"基准"(pivot)。
  2. 重新排序数列,所有比基准值小的元素摆放在基准前面,所有比基准值大的元素摆在基准后面(相同的数可以到任一边)。在这个分区结束之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
  3. 递归地(recursively)把小于基准值元素的子数列和大于基准值元素的子数列排序。

递归到最底部时,数列的大小是零或一,也就是已经排序好了。这个算法一定会结束,因为在每次的迭代(iteration)中,它至少会把一个元素摆到它最后的位置去。

img

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public void fastSort(int[] a, int lo, int hi) {
        if (lo >= hi) {
            return;
        }
        int left = lo;
        int right = hi;

        //保存基准值
        int pivot = a[left];
        while (left < right) {
            //从后向前找到比基准小的元素
            while (left < right && a[right] >= pivot)
                right--;
            a[left] = a[right];
            //从前往后找到比基准大的元素
            while (left < right && a[left] <= pivot)
                left++;
            a[right] = a[left];
        }
        // 放置基准值,准备分治递归快排
        a[left] = pivot;
        fastSort(a, lo, left - 1);
        fastSort(a, left + 1, hi);
    }

以下是快速排序算法复杂度:

平均时间复杂度 最好情况 最坏情况 空间复杂度 稳定性
O(nlog₂n) O(nlog₂n) O(n²) O(1) 不稳定

归并排序

算法描述

  1. 将序列每相邻两个数字进行归并操作,形成 floor(n/2)个序列,排序后每个序列包含两个元素;
  2. 将上述序列再次归并,形成 floor(n/4)个序列,每个序列包含四个元素;
  3. 重复步骤2,直到所有元素排序完毕。

img

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
public class Merge {

    //归并所需的辅助数组
    private static int[] aux;

    public static void sort(int[] a) {
        //一次性分配空间
        aux = new int[a.length];
        sort(a, 0, a.length - 1);
    }

    public static void sort(int[] a, int low, int high) {
        if (low >= high) {
            return;
        }
        int mid = (low + high) / 2;
        //将左半边排序
        sort(a, low, mid);
        //将右半边排序
        sort(a, mid + 1, high);
        merge(a, low, mid, high);
    }

    /**
     * 该方法先将所有元素复制到aux[]中,然后在归并会a[]中。方法咋归并时(第二个for循环)
     * 进行了4个条件判断:
     * - 左半边用尽(取右半边的元素)
     * - 右半边用尽(取左半边的元素)
     * - 右半边的当前元素小于左半边的当前元素(取右半边的元素)
     * - 右半边的当前元素大于等于左半边的当前元素(取左半边的元素)
     * @param a
     * @param low
     * @param mid
     * @param high
     */
    public static void merge(int[] a, int low, int mid, int high) {
        //将a[low..mid]和a[mid+1..high]归并
        int i = low, j = mid + 1;
        for (int k = low; k <= high; k++) {
            aux[k] = a[k];
        }

        for (int k = low; k <= high; k++) {
            if (i > mid) {
                a[k] = aux[j++];
            } else if (j > high) {
                a[k] = aux[i++];
            } else if (aux[j] < aux[i]) {
                a[k] = aux[j++];
            } else {
                a[k] = aux[i++];
            }
        }
    }

}

以下是归并排序算法复杂度:

平均时间复杂度 最好情况 最坏情况 空间复杂度 稳定性
O(nlog₂n) O(nlog₂n) O(nlog₂n) O(n) 稳定

堆排序

算法描述

  1. 先将初始序列𝐾[1..𝑛]建成一个大顶堆, 那么此时第一个元素𝐾1最大, 此堆为初始的无序区.
  2. 再将关键字最大的记录𝐾1 (即堆顶, 第一个元素)和无序区的最后一个记录 𝐾𝑛 交换, 由此得到新的无序区𝐾[1..𝑛−1]和有序区𝐾[𝑛], 且满足𝐾[1..𝑛−1].𝑘𝑒𝑦𝑠⩽𝐾[𝑛].𝑘𝑒𝑦
  3. 交换𝐾1 和 𝐾𝑛 后, 堆顶可能违反堆性质, 因此需将𝐾[1..𝑛−1]调整为堆. 然后重复步骤2, 直到无序区只有一个元素时停止。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public int[] sortArray(int[] nums) {
  for(int i = nums.length-1; i>0;i--) {
    // 每次都重新构建一次堆
    max_heapify(nums, i);

    // 将堆顶放到最后,表示已经确定排序了
    int tmp = nums[0];
    nums[0] = nums[i];
    nums[i] = tmp;
  }
  return nums;
}
public void max_heapify(int[] nums, int f) {
  int child;
  // 遍历所有父节点
  for(int i = (f-1)/2;i>=0;i--) {
    child = i*2+1;
    if(child!=f && nums[child+1]>nums[child]) {
      child++;
    }
    // 如果父节点比最大的子节点小就交换。
    if(nums[i]<nums[child]) {
      int tmp = nums[i];
      nums[i] = nums[child];
      nums[child] = tmp;
    }
  }
}
  1. 建立堆的过程, 从length/2 一直处理到0, 时间复杂度为O(n);
  2. 调整堆的过程是沿着堆的父子节点进行调整, 执行次数为堆的深度, 时间复杂度为O(lgn);
  3. 堆排序的过程由n次第2步完成, 时间复杂度为O(nlgn).
平均时间复杂度 最好情况 最坏情况 空间复杂度 稳定性
𝑂(𝑛log2𝑛)O(nlog2⁡n) 𝑂(𝑛log2𝑛)O(nlog2⁡n) 𝑂(𝑛log2𝑛)O(nlog2⁡n) 𝑂(1)O(1) 不稳定

基数排序

算法描述

从最低位开始,具体算法描述如下:

  1. 取得数组中的最大数,并取得位数;
  2. arr为原始数组,从最低位开始取每个位组成radix数组;
  3. 对radix进行计数排序(利用计数排序适用于小范围数的特点);
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public int[] sortArray(int[] nums) {
        sort(nums);
        return nums;
    }
    public static void sort(int[] arr) {
    if (arr.length <= 1) return;

    //取得数组中的最大数,并取得位数
    int max = 0;
    for (int i = 0; i < arr.length; i++) {
        if (max < arr[i]) {
            max = arr[i];
        }
    }
    int maxDigit = 1;
    while (max / 10 > 0) {
        maxDigit++;
        max = max / 10;
    }
    //申请一个桶空间
    int[][] buckets = new int[10][arr.length];
    int base = 10;

    //从低位到高位,对每一位遍历,将所有元素分配到桶中
    for (int i = 0; i < maxDigit; i++) {
        int[] bktLen = new int[10];        //存储各个桶中存储元素的数量

        //分配:将所有元素分配到桶中
        for (int j = 0; j < arr.length; j++) {
            int whichBucket = (arr[j] % base) / (base / 10);
            buckets[whichBucket][bktLen[whichBucket]] = arr[j];
            bktLen[whichBucket]++;
        }

        //收集:将不同桶里数据挨个捞出来,为下一轮高位排序做准备,由于靠近桶底的元素排名靠前,因此从桶底先捞。
        // 以个位数为例,个位数越大,加入arr的时间就越迟。那么在之后判断十位数的时候,也会较迟的加入arr中,因此而产生相同十位数,个位数的不同
        int k = 0;
        for (int b = 0; b < buckets.length; b++) {
            for (int p = 0; p < bktLen[b]; p++) {
                arr[k++] = buckets[b][p];
            }
        }
        System.out.println("Sorting: " + Arrays.toString(arr));
        base *= 10;
    }
}

以下是基数排序算法复杂度,其中k为最大数的位数:

平均时间复杂度 最好情况 最坏情况 空间复杂度 稳定性
O(d*(n+r)) O(d*(n+r)) O(d*(n+r)) O(n+r) 稳定

其中,d 为位数,r 为基数,n 为原数组个数。在基数排序中,因为没有比较操作,所以在复杂上,最好的情况与最坏的情况在时间上是一致的,均为 O(d*(n + r))

希尔排序

算法描述

  1. 选择一个增量序列 t1,t2,……,tk,其中 ti > tj, tk = 1;
  2. 按增量序列个数 k,对序列进行 k 趟排序;
  3. 每趟排序,根据对应的增量 ti,将待排序列分割成若干长度为 m 的子序列,分别对各子表进行直接插入排序。仅增量因子为 1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。

效果如下:

img

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public static void sort(int[] a) {
    int length = a.length;
    int h = 1;
    while (h < length / 3) h = 3 * h + 1;
    for (; h >= 1; h /= 3) {
        for (int i = 0; i < a.length - h; i += h) {
            for (int j = i + h; j > 0; j -= h) {
                if (a[j] < a[j - h]) {
                    int temp = a[j];
                    a[j] = a[j - h];
                    a[j - h] = temp;
                }
            }
        }
    }
}

以下是希尔排序复杂度:

平均时间复杂度 最好情况 最坏情况 空间复杂度 稳定性
O(nlog2 n) O(nlog2 n) O(nlog2 n) O(1) 不稳定

应用场景

  • **冒泡排序:**优化后的冒泡排序可用于当数据已经基本有序,且数据量较小时;

  • **插入排序:**若数组基本有序且数据规模较小时,选用插入排序较好;

  • **希尔排序:**数据量较小且基本有序时;

  • **选择排序:**当数据规模较小时,选择排序性能较好;

  • **堆排序:**堆排序适合处理数据量大的情况,数据呈流式输入时用堆排序也很方便;

  • **归并排序:**数据量较大且要求排序稳定时;

  • **快速排序:**快速排序适合处理大量数据排序时的场景;

  • **基数排序:**基数排序虽然时间复杂度较低,但需要满足的条件较多,如果能满足限制条件与空间需求,基数排序自然很快。

  • 若n较小(如n≤50),可采用直接插入或直接选择排序。

    • 若序列初始状态基本有序,则直接插入和冒泡最佳,随机的快速排序也不错。插入排序对部分有序的数组很有效,所需的比较次数平均只有选择排序的一半。
    • 若n较大,则应采用时间复杂度为O(nlgn)的排序方法:快速排序、堆排序或归并排序。归并排序可以处理数百万甚至更大规模的数组,但是插入和选择做不到。归并排序的主要缺点是辅助数组所使用的额外空间和n的大小成正比。
      • 快速排序是目前基于比较的内部排序中被认为是最好的方法,当待排序的关键字是随机分布时,快速排序的平均时间最短;
      • 堆排序所需的辅助空间少于快速排序,并且不会出现快速排序可能出现的最坏情况。但这两种排序都是不稳定的。
      • 若要求排序稳定,则可选用归并排序。两两归并的排序算法并不值得提倡,通常可以将它和直接插入排序结合在一起使用。先利用直接插入排序求得较长的有序子文件,然后再两两归并之。因为直接插入排序是稳定的,所以改进后的归并排序仍是稳定的。
    • 快速排序的优点是原地排序(只需要一个很小的辅助栈),但是基准的选取是个问题,对于小数组,快速排序要比插入排序慢。
    • 堆排序的优点是在排序时可以将需要排序的数组本身作为堆,无需任何额外空间,与选择排序有些类似,但所需的比较要少得多,堆排序适合例如嵌入式系统或低成本移动设备中容量有限的场景。

空间复杂度最大的排序算法

堆排序 O(n+r),r是基数。也就是10

跳表

什么是跳表

多层的有序链表,越上层,节点数越少

跳表如何查询

从最上面那层开始,从左往右遍历,直到下一个节点比要查询的值大,往下一层走。循环。

跳表的实现结构

为什么用跳表而不用红黑树

跳表的复杂度和红黑树一样都是O(lgn)

但是并发条件下,红黑树插入和删除时,可能需要rebalance整棵树,而跳表更加局部。

红黑树、哈希表、堆

红黑树

红黑树的每个节点上都有存储位表示节点的颜色,可以是红(Red)或黑(Black)。

红黑树的特性: (1)每个节点或者是黑色,或者是红色。 (2)根节点是黑色。 (3)每个叶子节点(NIL)是黑色。 [注意:这里叶子节点,是指为空(NIL或NULL)的叶子节点!] (4)如果一个节点是红色的,则它的子节点必须是黑色的。 (5)==从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。==

注意: (01) 特性(3)中的叶子节点,是只为空(NIL或null)的节点。 (02) 特性(5),确保没有一条路径会比其他路径长出俩倍。==因而,红黑树是相对是接近平衡的二叉树。==

image-20210216221031160

==为什么有平衡二叉树了还需要红黑树==

  • 因为平衡二叉树太严格了,导致每次操作都需要左旋或者右旋来调整
  • 而红黑树是没没有那么严格的平衡树,不需要每次都调整

应用

红黑树主要是用来存储有序的数据的

数据

红黑树的时间复杂度为: O(lgn)

一棵含有n个节点的红黑树的高度至多为2log(n+1).

操作

https://blog.csdn.net/wuzhenwei0419/article/details/84554049

红黑树的左旋

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
    * 左旋
    */
static <K,V> TreeNode<K,V> rotateLeft(TreeNode<K,V> root,
                                        TreeNode<K,V> p) {
    //这里的p即上图的A节点,r指向右孩子即C,rl指向右孩子的左孩子即D,pp为p的父节点
    TreeNode<K,V> r, pp, rl;
    if (p != null && (r = p.right) != null) {
        if ((rl = p.right = r.left) != null)
            rl.parent = p;
        //将p的父节点的孩子节点指向r
        // 如果A就是根节点,那么C换上来之后,要把它变成黑色,因为根节点都是黑色的
        if ((pp = r.parent = p.parent) == null)
            (root = r).red = false;
        else if (pp.left == p)
            pp.left = r;
        else
            pp.right = r;
        //将p置为r的左节点
        r.left = p;
        p.parent = r;
    }
    return root;
}

红黑树的右旋

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 右旋
*/
static <K,V> TreeNode<K,V> rotateRight(TreeNode<K,V> root,
                                        TreeNode<K,V> p) {
        //这里的p即上图的A节点,l指向左孩子即C,lr指向左孩子的右孩子即E,pp为p的父节点
    TreeNode<K,V> l, pp, lr;
    if (p != null && (l = p.left) != null) {
        if ((lr = p.left = l.right) != null)
            lr.parent = p;
        if ((pp = l.parent = p.parent) == null)
            (root = l).red = false;
        else if (pp.right == p)
            pp.right = l;
        else
            pp.left = l;
        l.right = p;
        p.parent = l;
    }
    return root;
}

哈希表

通过哈希函数将key进行映射。

如何解决哈希冲突

开放地址

如果该地址已经存了,那就往后继续探测,直到找到空地址位置,存入数据。

在查询的时候,如果key对不上,则按相同的规则继续探测。

链表法

如果地址冲突了,把数据当作链表连接起来,存在同一个地址内。

查询的时候需要遍历链表

堆就是以二叉树的形式来维护一个数组。

分为大根堆和小根堆。大根堆父节点比子节点要大。

二叉树的遍历

前序遍历

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public List<Integer> ans = new ArrayList<>();
public List<Integer> preorderTraversal(TreeNode root){
  preorder(root);
  return ans;
}

public void preorder(TreeNode node) {
  if(node == null) return;
  ans.add(node.val);
  preorder(node.left);
  preorder(node.right);
}

中序遍历

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public List<Integer> ans = new ArrayList<>();
public List<Integer> inorderTraversal(TreeNode root){
  inorder(root);
  return ans;
}

public void inorder(TreeNode node) {
  if(node == null) return;
  inorder(node.left);
  ans.add(node.val);
  inorder(node.right);
}

后序遍历

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public List<Integer> ans = new ArrayList<>();
public List<Integer> postorderTraversal(TreeNode root){
  postorder(root);
  return ans;
}

public void postorder(TreeNode node) {
  if(node == null) return;
  postorder(node.left);
  postorder(node.right);
  ans.add(node.val);
}