【数据结构】树

191 阅读8分钟

本文介绍的相关树结构主要有:二分搜索树、AVL树、2-3树、红黑树、二叉堆、线段树、字典树、并查集

知识点基于慕课网liuyubobobo讲师的视频

1. 相关概念

  1. 满二叉树:除最后一层无任何子节点外,每一层上的所有结点都有两个子结点的二叉树。
  2. 完全二叉树:对于深度为k的,有n个结点的二叉树,当其层序编号(按层来编号)与深度k的满二叉树的层序编号相同时,称为完全二叉树。所有的叶子节点都出现在最下面两层。其节点数的取值范围为:2^(k-1)-1 < n <= 2^k-1
  3. 平衡二叉树:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树
  4. 二叉树的性质(参考资料):
    • 在第i层上最多有2^(i-1)个节点
    • 深度为k的二叉树最多一共有2^k-1个节点
    • 设度为0(即叶子节点)的节点数为n0,度为2的节点数为n2,则有n0 = n2 + 1
    • 具有n个节点的完全二叉树的深度为 ⌊㏒2(n)⌋ ,即log 2为底n的对数再向下取整

2. 二分搜索树

  • 二分搜索树(Binary Search Tree)的各个节点的值:大于其左子树的所有节点的值,小于大于其右子树的所有节点的值
  • 二分搜索树的子树也是二分搜索树
  • 存储的数据要有可比性
  • 中序遍历的结果则是按小到大排序
  • 对于二分搜索树来说,添加、查找、删除的是与它的最大深度k有关的,就是对于这些操作最多也只需要遍历k次。假如是满二叉树,n = 2^k - 1,k = ㏒2(n+1),时间复杂度为O(㏒2(n)),也就是O(㏒n),因为以多少为底都是无关紧要的
  • 假如添加元素的顺序不恰当,就会使树退化成链表,而AVL树可以解决这个问题

3. AVL树

  • AVL树本质上是具有平衡功能的二分搜索树
  • 平衡因子:左右子树的高度差
  • 只要某节点的平衡因子大于1,而该树就出现了不平衡现象,就要通过左旋或右旋来解决,共有四种情况:LL、RR、LR、RL

假如AVL树的节点结构如下所示:

private class Node {
    public K key;
    public V value;
    // 以该节点作为根的树的高度
    public int height;

    public Node left, right;

    public Node(K key, V value) {
        this.key = key;
        this.value = value;
        left = null;
        right = null;
        height = 1;
    }
}
// 根节点
private Node root;
// 树大小
private int size;

先定义一些辅助方法,如下所示:

// 查找以node为根的树的最小节点
private Node searchMinNode(Node node) {
    if (node.left == null) {
        return node;
    } else {
        return searchMinNode(node.left);
    }
}

// 获取平衡因子,可根据正负来判断左右子树谁高谁低
private int getBalanceFactor(Node node) {
    if (node != null) {
        return getHeight(node.left) - getHeight(node.right);
    }
    return 0;
}

// 获取以node为根的树的高度
private int getHeight(Node node) {
    if (node != null) {
        return node.height;
    }
    return 0;
}

// 对节点y进行向右旋转操作,返回旋转后新的根节点x
//        y                              x
//       / \                           /   \
//      x   T4     向右旋转 (y)        z     y
//     / \       - - - - - - - ->    / \   / \
//    z   T3                       T1  T2 T3 T4
//   / \
// T1   T2
private Node rightRotate(Node y) {
    Node x = y.left;
    Node T3 = x.right;
    x.right = y;
    y.left = T3;
    y.height = 1 + Math.max(y.left.height, y.right.height);
    x.height = 1 + Math.max(x.left.height, x.right.height);
    return x;
}

// 对节点y进行向左旋转操作,返回旋转后新的根节点x
//    y                             x
//   / \                          /   \
// T1   x      向左旋转 (y)       y     z
//     / \   - - - - - - - ->   / \   / \
//   T2   z                    T1 T2 T3 T4
//       / \
//      T3 T4
private Node leftRotate(Node y) {
    Node x = y.right;
    Node T2 = x.left;
    x.left = y;
    y.right = T2;
    y.height = Math.max(getHeight(y.left), getHeight(y.right)) + 1;
    x.height = Math.max(getHeight(x.left), getHeight(x.right)) + 1;
    return x;
}

3.1 添加操作

  添加了某节点后,不平衡现象的只会发生在该节点的父辈和祖父辈上(从该节点到根节点的这条路径上所经过的节点),就要对这些节点进行平衡因子的判断并调整平衡。删除元素时同理

public void add(K key, V value) {
    root = addNode(root, key, value);
}

private Node addNode(Node node, K key, V value) {
    if (node == null) {
        size++;
        return new Node(key, value);
    } else {
        if (key.compareTo(node.key) < 0) {
            node.left = addNode(node.left, key, value);
        } else if (key.compareTo(node.key) > 0) {
            node.right = addNode(node.right, key, value);
        } else {
            node.value = value;
        }

        node.height = 1 + Math.max(getHeight(node.left), getHeight(node.right));

        int balanceFactor = getBalanceFactor(node);

        // LL
        if (balanceFactor > 1 && getBalanceFactor(node.left) >= 0) {
            return rightRotate(node);
        }
        // RR
        if (balanceFactor < -1 && getBalanceFactor(node.right) <= 0) {
            return leftRotate(node);
        }
        // LR
        if (balanceFactor > 1 && getBalanceFactor(node.left) < 0) {
            node.left = leftRotate(node.left);
            return rightRotate(node);
        }
        // RL
        if (balanceFactor < -1 && getBalanceFactor(node.right) > 0) {
            node.right = rightRotate(node.right);
            return leftRotate(node);
        }

        return node;
    }
}

3.2 删除操作

public void remove(K key) {
   root = removeNode(root, key);
}

private Node removeNode(Node node, K e) {
   // 没有找到该元素
   if (node == null) {
       return null;
   }
   Node retNode;
   if (e.compareTo(node.key) < 0) {
       node.left = removeNode(node.left, e);
       retNode = node;
   } else if (e.compareTo(node.key) > 0) {
       node.right = removeNode(node.right, e);
       retNode = node;
   } else {
       // 待删节点的左子树为空,或者待删节点是叶子节点
       if (node.left == null) {
           Node rightNode = node.right;
           node.right = null;
           size--;
           retNode = rightNode;
       } else if (node.right == null) {
           // 待删节点的右子树为空
           Node leftNode = node.left;
           node.left = null;
           size--;
           retNode = leftNode;
       } else {
           // 待删节点的左右子树均不为空
           // 1. 先找出比待删节点稍大的节点,称为后继者
           Node successor = searchMinNode(node.right);
           // 2.1. 先删除后继者原来位置的那个节点
           // 2.2. 再把待删节点替换为后继者
           successor.right = removeNode(node.right, successor.key);
           successor.left = node.left;
           node.right = node.left = null;
           retNode = successor;
       }
   }
   
   // 当删除的节点是叶子节点时,retNode将会是null,此时直接返回null
   if (retNode == null) {
       return null;
   }
   
   retNode.height = 1 + Math.max(getHeight(retNode.left), getHeight(retNode.right));
   int balanceFactor = getBalanceFactor(retNode);
   // LL
   if (balanceFactor > 1 && getBalanceFactor(retNode.left) >= 0) {
       return rightRotate(retNode);
   }
   // RR
   if (balanceFactor < -1 && getBalanceFactor(retNode.right) <= 0) {
       return leftRotate(retNode);
   }
   // LR
   if (balanceFactor > 1 && getBalanceFactor(retNode.left) < 0) {
       retNode.left = leftRotate(retNode.left);
       return rightRotate(retNode);
   }
   // RL
   if (balanceFactor < -1 && getBalanceFactor(retNode.right) > 0) {
       retNode.right = rightRotate(retNode.right);
       return leftRotate(retNode);
   }
   return retNode;
}

4. 2-3树

  • 满足二分搜索树的基本性质

  • 每个节点可以存放一个元素或者两个元素,每个节点有2个或者3个孩子 024-2-3树节点.png

  • 2-3树是一种绝对平衡的树,从根节点到叶子节点的节点数量是相同的

  • 2-3树在添加元素的同时也要满足绝对平衡

例子1:根节点为12,往根节点添加元素6 024-例子1.png

例子2:紧接例子1,再添加元素2,此时融合后的根节点暂时存了3个元素,于是拆分 024-例子2.png

例子3:当融合后的元素个数是3且是叶子节点时,则先拆分再向父节点融合。如下图往第一棵树添加元素4 024-例子3.png

例子4:当向父节点融合后,父节点存了3个元素,则父节点再拆分。如下图往第一棵树添加元素4 024-例子4.png

5. 红黑树

  红黑树与2-3树相似,它们节点对应关系如下所示 024-红黑树.png 024-红黑树2.png

于是红黑树有以下性质:

  • 根节点是黑色的
  • 每一个**叶子节点(这里的叶子节点指的是最后的空节点)**是黑色的
  • 如果一个节点是红色的,那么它的孩子节点是黑色的
  • 黑色节点的右孩子一定是黑色的
  • 从任意一个节点到叶子节点,经过的黑色节点是一样的,即黑平衡

5.1 添加操作

  最复杂的添加有三种情况

  1. 如下图所示,当初始状态如①所示,当添加的元素比红色节点的大时,顺序则是①②③④⑤ 024-红黑树添加.png

  2. 若添加的元素比红色节点的小时,则是直接从①跳到③

  3. 若添加到元素比黑色节点的大时,则是直接从①跳到④

public class RedBlackTree<K extends Comparable<K>, V> {
    private static final boolean RED = true;
    private static final boolean BLACK = false;

    private class Node {
        public K key;
        public V value;
        public int height;

        public boolean color;

        public Node left, right;

        public Node(K key, V value) {
            this.key = key;
            this.value = value;
            left = null;
            right = null;
            color = RED;
        }
    }

    private Node root;

    private int size;

    public boolean isRed(Node node) {
        if (node == null) {
            return BLACK;
        }
        return node.color;
    }

    public void add(K key, V value) {
        root = addNode(root, key, value);
        root.color = BLACK;
    }

    /**
     * 添加元素时,新元素的初始状态是红色,然后再根据情况改变颜色
     */
    private Node addNode(Node node, K key, V value) {
        if (node == null) {
            size++;
            return new Node(key, value);
        }

        if (key.compareTo(node.key) < 0) {
            node.left = addNode(node.left, key, value);
        } else if (key.compareTo(node.key) > 0) {
            node.right = addNode(node.right, key, value);
        } else {
            node.value = value;
        }

        if (isRed(node.right) && !isRed(node.left)) {
            node = leftRotate(node);
        }

        if (isRed(node.left) && isRed(node.left.left)) {
            node = rightRotate(node);
        }

        if (isRed(node.left) && isRed(node.right)) {
            flipColor(node);
        }

        return node;
    }

    //   node                           x
    //   / \                          /   \
    // T1   x        左旋转         node   T3
    //     / \   - - - - - - - ->   / \
    //   T2  T3                    T1 T2
    private Node leftRotate(Node node) {
        Node x = node.right;

        node.right = x.left;
        x.left = node;

        x.color = node.color;
        node.color = RED;
        return x;
    }

    //       node                            x
    //       / \                           /   \
    //      x   T3    右旋转               T1  node
    //     / \       - - - - - - - ->          / \
    //    T1 T2                               T2 T3
    private Node rightRotate(Node node) {
        Node x = node.left;

        node.left = x.right;
        x.right = node;

        x.color = node.color;
        node.color = RED;
        return x;
    }

    // 翻转颜色
    private void flipColor(Node node) {
        node.color = RED;
        node.left.color = BLACK;
        node.right.color = BLACK;
    }
}

5.2 与AVL树比较

  • 当查询的频率高时,AVL树性能好
  • 红黑树的最大高度可以是2㏒n(一直沿着红色节点走),并不满足平衡二叉树的定义,只是黑平衡
  • 添加和删除操作比AVL树好,综合性能更好

6. 二叉堆

  • 二叉堆是一颗完全二叉树
  • 堆中的某节点的值不大于其父节点的值,即根节点的值就是最大值,这种堆称为最大堆。反过来定义则是最小堆。注意,节点的值的大小与其所在层数是没有关系的
  • 当使用数组存储二叉堆时,根节点放在索引0位置,若某节点的索引为 i ,则它的左子树和右子树的索引分别为 i*2+1 以及 i*2+2,它的父节点索引为 ⌊(i - 1)/2⌋
  • 基于这种堆可以十分简单地实现优先队列
  • Java提供的优先队列PriorityQueue是最小堆

假如堆是基于动态数组实现的,自定义一些辅助方法:

// 获取父节点索引
private int getParent(int index) {
    if (index != 0) {
        return (index - 1) / 2;
    } else {
        throw new IllegalArgumentException("Root doesn't have parent.");
    }
}

// 获取左子树索引
private int getLeftChild(int index) {
    return index * 2 + 1;
}

// 获取右子树索引
private int getRightChild(int index) {
    return index * 2 + 2;
}

6.1 添加操作

public void add(E e) {
    // 往数组末尾添加元素
    array.addLast(e);

    // 上浮操作,使其满足堆的性质:堆中的某节点的值不大于其父节点的值
    siftUp(getSize() - 1);
}

// 对某个索引的值进行上浮操作
private void siftUp(int index) {
    while (index > 0 && array.get(getParent(index)).compareTo(array.get(index)) < 0) {
        // 交换数组中的两个索引的值
        array.exchange(index, getParent(index));
        index = getParent(index);
    }
}

6.2 删除操作

// 取出最大元素,并从堆中删除
public E extractMax() {
    E result = peekMax();
    // 交换第一个元素(根节点)和末尾元素
    array.exchange(0, getSize() - 1);
    
    // 删除末尾元素
    array.removeLast();
    
    // 对第一个元素进行下沉操作
    siftDown(0);
    return result;
}

public E peekMax() {
    if (!isEmpty()) {
        return array.get(0);
    } else {
        throw new IllegalArgumentException("This is an empty heap.");
    }
}

// 对某个索引的值进行下沉操作
private void siftDown(int index) {
    while (getLeftChild(index) < getSize()) {
        int max = getLeftChild(index);
        // 如果当前节点有右孩子 并且 右孩子大于左孩子
        if (max + 1 < getSize()
                && array.get(max + 1).compareTo(array.get(max)) > 0) {
            max = max + 1;
        }
        // 此时max索引的值是左右孩子当中的最大值
        if (array.get(index).compareTo(array.get(max)) < 0) {
            array.exchange(index, max);
            index = max;
        } else {
            break;
        }
    }
}

7. 线段树(区间树)

  • 二叉树的每个节点存储的都是某个区间范围的信息,叶子节点可以只存储一个元素的信息,也可以存储某个更小区间的信息
  • 假如总区间有n个元素,那么使用数组存储线段树时,好的情况需要2*n的空间;坏情况需要4*n的空间,即当n不是2的次幂时,就会造成某些节点的左子树存了一个元素的信息,右子树却存了两个元素的信息,这种情况就需要4*n的空间 024-线段树.png

简单实现的代码如下所示:

// 融合器
public interface Merger<E> {
    E merger(E a, E b);
}

// 线段树
public class SegmentTree<E> {
	// 存放原始数据
    private E[] data;

    // 真正的线段树
    private E[] tree;

    private Merger<E> merger;

    public SegmentTree(E[] arr, Merger<E> merger) {
        data = (E[]) new Object[arr.length];
        for (int i = 0; i < data.length; i++) {
            data[i] = arr[i];
        }
        this.merger = merger;
        tree = (E[]) new Object[4 * arr.length];
        buildSegmentTree(0, 0, data.length - 1);
    }

    public int getSize() {
        return data.length;
    }

    public E get(int index) {
        if (index < 0 || index >= data.length) {
            throw new IllegalArgumentException("Illegal parameter 'index'.");
        }
        return data[index];
    }

    public void set(int index, E e) {
        if (index < 0 || index >= data.length) {
            throw new IllegalArgumentException("Illegal parameter 'index'.");
        } else {
            data[index] = e;
            update(0, 0, data.length - 1, index, e);
        }
    }

    private void update(int treeIndex, int l, int r, int index, E e) {
        if (l == r) {
            tree[treeIndex] = e;
            return;
        } else {
            int mid = l + (r - l) / 2;
            int leftChildIndex = getLeftChild(treeIndex);
            int rightChildIndex = getRightChild(treeIndex);
            if (index >= mid + 1) {
                update(rightChildIndex, mid + 1, r, index, e);
            } else {
                update(leftChildIndex, l, mid, index, e);
            }
            tree[treeIndex] = merger.merger(tree[leftChildIndex], tree[rightChildIndex]);
        }
    }

    /**
     * 查询区间为[queryLeft, queryRight]的信息
     */
    public E query(int queryLeft, int queryRight) {
        if (queryLeft > queryRight || queryLeft < 0 || queryLeft >= data.length
                || queryRight >= data.length) {
            throw new IllegalArgumentException("Illegal parameter 'left' or 'right'.");
        } else {
            return queryFromChild(0, 0, data.length - 1, queryLeft, queryRight);
        }

    }

    private E queryFromChild(int index, int l, int r, int queryLeft, int queryRight) {
        if (l == queryLeft && r == queryRight) {
            return tree[index];
        } else {
            int leftChildIndex = getLeftChild(index);
            int rightChildIndex = getRightChild(index);

            // 这种写法是避免l+r的结果会越界
            int mid = l + (r - l) / 2;

            if (mid < queryLeft) {
                return queryFromChild(rightChildIndex, mid + 1, r, queryLeft, queryRight);
            } else if (mid >= queryRight) {
                return queryFromChild(leftChildIndex, l, mid, queryLeft, queryRight);
            } else {
                E leftResult = queryFromChild(leftChildIndex, l, mid, queryLeft, mid);
                E rightResult = queryFromChild(rightChildIndex, mid + 1, r, mid + 1, queryRight);
                return merger.merger(leftResult, rightResult);
            }
        }
    }

    /**
     * 创建线段树,在tree[index]处,存放的是区间为data[l,r]的信息
     */
    private void buildSegmentTree(int index, int l, int r) {
        if (l == r) {
            tree[index] = data[l];
            return;
        } else {
            int leftChildIndex = getLeftChild(index);
            int rightChildIndex = getRightChild(index);

            // 这种写法是避免l+r的结果会越界
            int mid = l + (r - l) / 2;

            buildSegmentTree(leftChildIndex, l, mid);
            buildSegmentTree(rightChildIndex, mid + 1, r);

            // 根据融合器来操作区间元素,如求和,求最大值等
            // 不能求均值,这里的代码逻辑不符合数学上的逻辑
            tree[index] = merger.merger(tree[leftChildIndex], tree[rightChildIndex]);
        }
    }

    private int getLeftChild(int index) {
        return 2 * index + 1;
    }

    private int getRightChild(int index) {
        return 2 * index + 2;
    }
}

8. 字典树

  字典树常用于处理字符串,并不是一个二叉树,如下图所示的字典树中,就存储了"cat"、"dog"、"pan"和"panda"

024-字典树.png

简单实现的代码如下所示:

public class Trie {
    private Node root;

    private int size;

    private class Node {
        /**
         * 判断从根到该节点能否组成一个单词
         */
        public boolean isWord;

        public TreeMap<Character, Node> branch;

        public Node(boolean isWord) {
            this.isWord = isWord;
            branch = new TreeMap<>();
        }

        public Node() {
            this(false);
        }
    }

    public Trie() {
        root = new Node();
        size = 0;
    }

    public int getSize() {
        return size;
    }

    /**
     * 向字典树中添加一个单词
     */
    public void add(String word) {
        Node cur = root;
        for (int i = 0; i < word.length(); i++) {
            char c = word.charAt(i);
            if (cur.branch.get(c) == null) {
                cur.branch.put(c, new Node());
            }
            cur = cur.branch.get(c);
        }
        // 以前不存在该单词,则size++
        if (!cur.isWord) {
            cur.isWord = true;
            size++;
        }
    }

    /**
     * 判断字典树中是否存在某单词
     */
    public boolean contains(String word) {
        Node cur = root;
        for (int i = 0; i < word.length(); i++) {
            cur = cur.branch.get(word.charAt(i));
            if (cur == null) {
                return false;
            }
        }
        return cur.isWord;
    }

    /**
     * 查询是否存在以特定字符串为前缀的单词
     */
    public boolean containsPrefix(String prefix) {
        Node cur = root;
        for (int i = 0; i < prefix.length(); i++) {
            cur = cur.branch.get(prefix.charAt(i));
            if (cur == null) {
                return false;
            }
        }
        return true;
    }
}

9. 并查集

  有这么一类问题,假如A和B是朋友,B和C是朋友,那么A和C也是朋友,即“朋友”关系具有传递性。那么假如有一大群人,如何快速判断任意两个人是不是朋友关系呢?这时就需要并查集数据结构。而且判断一张图是否存在环也需要用上并查集。

  并查集与一般树结构不同的地方在于:子节点指向父节点

9.1 实现思路

  1. 初始化时,让每个元素的父节点都指向自己
  2. 连接某两个元素A和B时,可以先递归找到A和B的根节点,然后让A的根节点指向B的根节点
  3. 对于任意两个元素,只需判断它们的根节点是否相同,就能得出这两个节点是否有连接关系

9.2 实现代码

public class UnionFind {

    /**
     * 记录各个元素的父节点
     * 1.若parent[i] = j, 则说明i的父节点是j
     * 2.若parent[i] = i, 则说明此时i是根节点
     */
    private int[] parent;

    public UnionFind(int capacity) {
        parent = new int[capacity];
        // 初始时,每个节点都是一颗独立的树
        for (int i = 0; i < capacity; i++) {
            parent[i] = i;
        }
    }

    public int getCapacity() {
        return parent.length;
    }

    public boolean isConnected(int e1, int e2) {
        validateElement(e1);
        validateElement(e2);

        return findRoot(e1) == findRoot(e2);
    }

    public void union(int e1, int e2) {
        validateElement(e1);
        validateElement(e2);

        int root1 = findRoot(e1);
        int root2 = findRoot(e2);
        parent[root2] = root1;
    }

    private int findRoot(int e) {
        if (parent[e] == e) {
            return e;
        }
        // 路径压缩
        parent[e] = findRoot(parent[e]);
        return parent[e];
    }

    private void validateElement(int e) {
        if (e < 0 || e >= parent.length) {
            throw new IllegalArgumentException("Out of bound!");
        }
    }
}

10. 相关链接