二叉查找树

特点

每个结点的键值大于左孩子,小于右孩子

每个孩子又是二叉查找树

二分查找树不一定是完全二叉树

对于任何节点:

插入

if (root == null) {    count++;    return new Node(key, value); // 当前节点为null,则创建一个节点返回}if (key.equals(root.key)) { // 当前节点等于要插入的节点,则直接覆盖    root.value = value;} else if (less(key, root.key)) { // 当前节点比要插入的大,则向当前节点的左子树插入    root.left = insert(root.left, key, value);} else if (greater(key, root.key)) {  // 当前节点比要插入的小,则向当前节点的右子树插入    root.right = insert(root.right, key, value);}

查找

原理同插入,根据左子树比父节点小,右子树比父节点大的条件

if (root == null){    return null;}if (key.equals(root.key)){    return root.value;}else if(less(key,root.key)){    return search(root.left,key);}else {    return search(root.right,key);}

floor与ceil

遍历

先访问当前节点,再递归访问左右子树

if (root != null){    consumer.accept(root.key,root.value);    preOrder(root.left,consumer);    preOrder(root.right,consumer);}

先递归访问左子树,再访问自身,再递归访问右子树

if (root != null){    preOrder(root.left,consumer);    consumer.accept(root.key,root.value);    preOrder(root.right,consumer);}

先递归访问左右子树,在访问自身

if (root != null){    preOrder(root.left,consumer);    preOrder(root.right,consumer);    consumer.accept(root.key,root.value);}
Queue<Node> queue = new LinkedList<>();queue.add(root);while (!queue.isEmpty()) {    var node = queue.remove();    consumer.accept(node.key,node.value);    if (node.left != null){        queue.add(node.left);    }    if (node.right != null){        queue.add(node.right);    }}

删除

分为三种情况

private Node removeMax(Node node) {        if (node.right == null) {        // 代表当前节点就是最大节点,所以返回当前节点的左子树给父节点        count--;        return node.left;    }    // 将删除的节点的左子树作为父节点的右子树    node.right = removeMax(node.right);    return node;}

Hubbard Deletion

使用被删除节点右子树中的最小节点来代替被删除节点

局限性

平衡二叉树

AVL树

在增加和删除节点时通过旋转来保持平衡

右旋:以某个节点为中心 将它沉入当前右子节点的位置 然后让当前左子节点作为新树的根

屏幕截图 2020-09-22 120018

左旋:

屏幕截图 2020-09-22 120118

2-3查找树

20227816630

搜索

搜索的过程和二叉树并没有太多的区别,只是遇到 3 节点的时候,多判断一次是否介于 a、b 之间

插入

2-3树之所以完美平衡,关键在于插入时的维护

插入的时候节点分裂

删除

红黑树

红黑树,正是采用标准的二叉查找树节点附着上额外的颜色信息来表示 2-3 树的实现,每一个红色节点都和它的父亲节点一起,构成了一个 3 节点的模拟,通过旋转操作完成 2-3 节点的合并和分裂,从而在不改变二叉树节点结构的前提下,保证二叉树的有序性和平衡性

红黑树不追求左右子树高度差不超过1,而是保证从根节点到叶尾的最长路径不超过最短路径的2倍,高度近似 log2n,是近似平衡的,插入、删除、查找操作的时间复杂度都是 O(logn)

其他约束条件:

红黑树的任何旋转至多3次就能完成

这些约束,都是为了保证每一颗红黑树和 2-3 Tree 是一一对应的

20227816326

基本操作

旋转

左旋:本质上就是将某个 3 节点从以较小的键为根转移成较大的键为根

202278164648

反色

插入

新插入的节点均设为红色

黑节点插入

202278164949

红节点插入

202278165136

20227816521

颜色反转

删除

可被应用优先级队列

堆的存储

二叉堆

大顶堆就要求堆中所有节点的值,一定大于其左右子树中的任何一个节点的值,小顶堆就正好相反

优先级队列适合用大顶堆实现,这样每次只需要从顶部取出元素即可获得优先级最高的元素

用数组存储二叉堆

stateDiagram-v2  62 --> 41  62 --> 30  41 --> 28  41 --> 16  30 --> 22  30 --> 13  28 --> 19  28 --> 17  16 --> 15
[62,41,30,28,16,22,13,19,17,15]

操作

新加入的元素与其父元素判断,是否比父元素大,如果是,交换两个元素,以此类推,直到小于其父亲

while (less(data,i/2,i)) {    swap(data,i/2,i);    i/=2;}

只能取出根节点的元素,取出后,使用堆中的最后一个元素填补空缺

填补后,跟左右两个孩子比较,哪个孩子大就跟谁交换...以此类推,直至自己比两个孩子都大

while (2 * k <= count) {    int j =2*k;    // 确定要跟左子树比较还是跟右子树    if (j+1<=count && greater(data,j+1,j)){        // 右子树        j++;    }    // 如果自己大于要比较的子树,则停止    if (greaterThan(data,k,j)){        break;    }    swap(data,k,j);    k=j;}

将根节点删除后,我们把二叉堆中最后的元素提到根节点的位置,这样又可以保证新的二叉树是一颗满二叉树了,然后要做的比较 + 交换

堆排序

MaxHeap<Comparable<?>> heap = new MaxHeap<>(a.length+1);for (int i = 0; i < a.length; i++) {    heap.insert(a[i]);}for (int i = 0; i < a.length; i++) {    a[i]=heap.remove();}

Heapify

堆化:将数组转为堆

对于一棵完全二叉树,其最后一个非叶子节点是元素个数除二取整

所以要把一个数组堆化,只需要对其非叶子结点进行shift down

for (int i = 0; i < a.length; i++) {    data[i + 1] = a[i];}count = a.length;// 对其非叶子结点进行shift downfor (int i = count / 2; i >= 1; i--) {    shiftDown(i);}

原地堆排序

int n = a.length;// 先将整个数组构造成一个最大堆for (int i = (n - 2) / 2; i >= 0; i--) {    shiftDown(a, n, i);}// 将堆中的第一大元素移到末尾,再次构造最大堆(排除末尾排好序的元素)// 然后下一次循环再将第一大元素移到倒数第二个...以此类推,直至只剩一个元素for (int i = n - 1; i > 0; i--) {    swap(a, 0, i);    shiftDown(a, i, 0);}

索引堆

并查集

并查集是一种树型的数据结构,用于处理一些不交集(Disjoint Sets)的合并及查询问题

find

int find(int p) {    return data[p];}

另外一种实现,通过判断两个节点是否拥有同样的祖先来判断是否相连

while (p != parent[p]) {    p = parent[p];}return p;

isConnected

boolean isConnected(int p, int q) {    return find(p) == find(q);}

union

int pid = find(p);int qid = find(q);if (pid == qid){    return;}for (int i = 0; i < count; i++) {    if (data[i]==pid){        data[i]=qid;    }}

使用另外一种实现的union

int qRoot = find(p);int pRoot = find(q);if (qRoot == pRoot){    return;}parent[pRoot]=qRoot;

基于size的优化,维护一个size数组,代表以i为根的集合的元素个数

if (sz[pRoot]<sz[qRoot]){            parent[pRoot] = qRoot;    sz[qRoot]+=sz[pRoot];}else {    parent[qRoot] = pRoot;    sz[qRoot]+=sz[pRoot];}

使用rank来决定谁连接谁

if (rank[pRoot] < rank[qRoot]) {    parent[pRoot] = qRoot;} else if ((rank[pRoot] > rank[qRoot])) {    parent[qRoot] = pRoot;} else {    parent[pRoot] = qRoot;    rank[qRoot] += 1;}

路径压缩

while (p != parent[p]) {    parent[p]=parent[parent[p]];    p = parent[p];}return p;