基本查找算法

1. 基于符号表/索引表的查找算法

        符号表目的就是将一个键和一个值联系起来,是一种存储键值对的数据结构。

1.1 基于二叉查找树实现的符号表/索引表

        二叉查找树是将二分查找的效率和链表的灵活性结合起来,但依赖结点(键)的分布足够随机。具体是使每个结点含有两个链接的二叉查找树,每个结点只能有一个父结点(除了根节点),而且每个结点都只有左右两个链接,分别指向左子结点和右子结点。特点是左子结点小于根结点,根结点小于右子结点。

public class BST<Key extends Comparable<Key>, Value> {
    private class Node {
        private Key key;//键
        private Value val;//值
        private Node left, right;//指向子树的链接
        private int N;//结点总数

        public Node(Key key, Value val, int n) {
            this.key = key;
            this.val = val;
            N = n;
        }
    }
    private Node root;//二叉查找树的根结点
    public int size() {
        return size(root);
    }
    public int size(Node x) {
        if (x == null) {
            return 0;
        } else {
            return x.N;
        }
    }
    public Value get(Key key) {
        return get(root, key);
    }
    public Value get(Node x, Key key) {
        //在以x为根结点的柿子树中查找并返回key对应的值,找不到为null
        if (x == null) {
            return null;
        }
        int cmp = key.compareTo(x.key);
        if (cmp < 0) {
            return get(x.left, key);
        } else if (cmp > 0) {
            return get(x.right, key);
        } else {
            return x.val;
        }
    }
    public void put(Key key, Value value) {
        //查找key,找到则更新,否则创建一个新的结点
        put(root, key, value);
    }
    private Node put(Node x, Key key, Value value) {
        //如果key存在于已x为根结点的子树中则更新它的value,否则插入一个新结点
        if (x == null) {
            return new Node(key, value, 1);
        }
        int cmp = key.compareTo(x.key);
        if (cmp < 0) {
            x.left = put(x.left, key, value);
        } else if (cmp > 0) {
            x.right = put(x.right, key, value);
        } else {
            x.val = value;
        }
        x.N = size(x.left) + size(x.right) + 1;
        return x;
    }
} 
1.1.1 最大键和最小键以及向上取整和向下取整
//最小键
public Key min(){
    return min(root).key;
}
private Node min(Node x){
    if (x.left==null){
        return x;
    }
    return min(x.left);
}
//小于等于key的最大键
public Key floor(Key key){
    Node x = floor(root,key);
    if (x==null){
        return null;
    }
    return x.key;
}
private Node floor(Node x, Key key){
    if (x==null){
        return null;
    }
    int cmp = key.compareTo(x.key);
    if (cmp==0){
        return x;
    }
    if (cmp<0){
        return floor(x.left,key);
    }
    Node t = floor(x.right, key);
    if (t !=null){
        return t;
    }else {
        return x;
    }
}
键的排名和排名k的键
//返回排名为k的键
public Key select(int k) {
    return select(root, k).key;
}
private Node select(Node x, int k) {
    if (x == null) {
        return null;
    }
    int t = size(x.left);
    if (t > k) {
        return select(x.left, k);
    } else if (t < k) {
        return select(x.right, k - t - 1);

    } else {
        return x;
    }
}
//返回给定键的排名
public int rank(Key key) {
    return rank(root, key);
}
private int rank(Node x, Key key) {
    //返回以x为根结点的子树中小于x.keyd的键的数量
    if (x == null) {
        return 0;
    }
    int cmp = key.compareTo(x.key);
    if (cmp < 0) {
        return rank(x.left, key);
    }else if (cmp>0){
        return 1+size(x.left)+rank(x.right,key);
    }else {
        return size(x.left);
    }
}
删除最小结点和删除某一结点
//删除最小键操作
public void deleteMin() {
    root = deleteMin(root);
}
private Node deleteMin(Node x) {
    if (x.left == null) {
        return x.right;
    }
    x.left = deleteMin(x.left);
    x.N = size(x.left) + size(x.right) + 1;
    return x;
}
//删除键操作,
public void delete(Key key) {
    root = delete(root, key);
}
//该算法不适合大规模
private Node delete(Node x, Key key) {
    /**
     * 在删除结点x后用它的后继结点(即x的右子树中的最小结点)填充它的位置,这样可以保证树的有序性
     * ①将删除接结点的链接保存在t
     * ②将x指向它的后继结点min(t.right)
     * ③将x的左链接设为t.left
     * ④将x的右链接指向deleteMiN(t.right)
     */
    if (x == null) {
        return null;
    }
    int cmp = key.compareTo(x.key);
    if (cmp < 0) {
        x.left = delete(x.left, key);
    } else if (cmp > 0) {
        x.right = delete(x.right, key);
    } else {
        if (x.right == null) {
            return x.left;
        }
        if (x.left == null) {
            return x.right;
        }
        Node t = x;
        x = min(t.right);
        x.right = deleteMin(t.right);
        x.left = t.left;
    }
    x.N = size(x.left) + size(x.right) + 1;
    return x;
}

1.2 平衡二叉查找树

        由于二叉查找树在最坏情况下的性能还是不能接受,我们需要保证二叉查找树的平衡性,才能得到解决且时间复杂度在对logN数级别。

        1.2.1 2-3平衡查找树

        2-3平衡查找树引入3-结点,它含有2个键和3条链接。特点:2-结点,左链接指向的键都小于2-结点的键,右链接指向的键都大于2-结点的键。3-节点,左链接指向的键都小于3-结点的2个键,中链接指向的键位于3-结点的2个键之间,右链接指向的键都大于3-结点的2个键。

        1. 向2-结点中插入新键:

                只要把2-结点替换为一个3-结点。

        2. 向3-结点中插入新键:

                把3-结点变为4-结点,再将4-结点分解为二叉树,高度加1。

        3. 向一个父结点为2-结点的3-结点中插入新键:

                把3-结点变为4-结点,再将4-结点分解为二叉树,然后将二叉树的第二大结点加入

                到2-结点的父结点,高度加1。

        4. 向一个父结点为3-结点的3-结点中插入新键(6种情况):

                把3-结点变为4-结点,将4-结点分解为二叉树,再将二叉树的第二大结点加入到3-

                结点的父结点,使之变为4-结点,再将父结点分为两个2-结点并将第二大键加入到

                父结点的父结点,高度加1。

注:4-结点的分解一次局部变换,不影响树的有序性和平衡性。

        5. 小结:

           2-3平衡查找树虽然再最坏情况下,也可以是logN对数级别的时间复杂度,但是实现

           起来相当复杂且产生很大的额外开销,所以就引出了红黑平衡二叉查找树来解决。

1.2.2 红黑平衡查找树

          基于2-3平衡查找树的思想,红黑平衡查找树对其进行优化。一个是替换3-结点,引出红链接和黑链接。

        红链接将两个2-结点连接起来构成一个3-结点。

        黑链接则是2-3平衡查找树的普通链接。

        时间复杂度除了范围查找外(所需额外的时间和返回的键的数量成正比),其他操作都是logN对数级别。

一:红黑树的定义:

        1. 红链接均为左链接

        2. 没有任何一个结点同时和两条红链接相连。

        3. 任意叶子结点到根节点的路径上的黑链接数量相同。

        4. 将红链接相连的结点合并,得到就是一颗2-3树。

二:红黑树的实现过程:

        每次插入的新结点,它的父结点指向它的链接变为红链接【注:指向根结点的链接一定得为黑链接)】,然后再经过旋转变化【注:如果一个结点的两个子结点都为红黑(红链接),那么需要将子结点都变为黑色并将它的父结点变为红色(指向它的链接变为红链接)】,再将根结点设为黑色,如果根结点由红变为黑,那么树的高度加1,最终得到完美红黑树。

        1. 向单个2-结点中插入新键和向树底部的2-结点插入一个新键:

        2. 向一个3-结点中插入新键(3中情况):

        3. 向树底部的3-结点中插入新键:

        4. 小结:

        ①如果右子结点是红色的而左自结点是黑色的,进行左旋转。

        ②如果左子结点是红色的且它的左子结点也是红色,进行右旋转。

        ③如果左右子结点都为红色,进行黑色转换,父结点变为红色。

        5. 代码实现:
public class RedBlackBST<Key extends Comparable<Key>, Value> {
    private final static boolean RED = true;
    private final static boolean BLACK = false;
    private Node root;
    private class Node {
        private boolean color;//由其父结点指向它的链接的颜色
        private Key key;//键
        private Value value;//值
        private RedBlackBST.Node left, right;//指向子树的链接
        private int N;//结点总数
        public Node(Key key, Value val, int n, boolean color) {
            this.key = key;
            this.value = val;
            N = n;
            this.color = color;
        }
    }
    private boolean isRed(Node x) {
        if (x == null) {
            return false;
        }
        return x.color == RED;
    }
    //左旋转
    private Node rotateLeft(Node h) {
        Node x = h.right;
        h.right = x.left;
        x.left = h;
        x.color = h.color;
        h.color = RED;
        x.N = h.N;
        h.N = 1 + size(h.left) + size(h.right);
        return x;
    }
    //右旋转
    private Node rotateRight(Node h) {
        Node x = h.left;
        h.left = x.right;
        x.right = h;
        x.color = h.color;
        h.color = RED;
        x.N = h.N;
        h.N = 1 + size(h.left) + size(h.right);
        return x;
    }
    public int size(Node x) {
        if (x == null) {
            return 0;
        } else {
            return x.N;
        }
    }
    //颜色转换(两个子链接由red变为black,指向它的链接变为red)
    private void flipColors(Node h) {
        h.color = RED;
        h.left.color = BLACK;
        h.right.color = BLACK;
    }
    //插入新键
    public void put(Key key, Value value) {
        //查找ket。找到则更新其值,否则创建一个新的结点
        root = put(root, key, value);
    }
    private Node put(Node h, Key key, Value value) {
        if (h == null) {
            return new Node(key, value, 1, RED);
        }
        int cmp = key.compareTo(h.key);
        if (cmp < 0) {
            h.left = put(h.left, key, value);
        } else if (cmp > 0) {
            h.right = put(h.right, key, value);
        } else {
            h.value = value;
        }
        if (isRed(h.right) && !isRed(h.left)) {
            h = rotateLeft(h);
        }
        if (isRed(h.left) && !isRed(h.left.left)) {
            h = rotateRight(h);
        }
        if (isRed(h.right) && isRed(h.left)) {
            flipColors(h);
        }
        h.N = size(h.left) + size(h.right) + 1;
        return h;
    }
    //删除最小值
    public void deleteMin() {
        if (!isRed(root.left) && !isRed(root.right)) {
            root.color = RED;
        }
        root = deleteMin(root);
        if (root != null) {
            root.color = BLACK;
        }
    }
    private Node deleteMin(Node h) {
        if (h.left == null) {
            return null;
        }
        if (!isRed(h.left) && !isRed(h.left.left)) {
            h = moveRedLeft(h);
        }
        h.left = deleteMin(h.left);
        return balance(h);
    }
    private Node moveRedLeft(Node h) {
        //假设结点h为红色,h.left和h.left.left都是黑色
        //将h.left或者h.left的子节点之一变为红
        flipColors(h);
        if (isRed(h.right.left)) {
            h.right = rotateRight(h.right);
            h = rotateLeft(h);
            flipColors(h);
        }
        return h;
    }
    private Node balance(Node h) {
        if (isRed(h.right)) {
            h = rotateLeft(h);
        }
        if (isRed(h.left) && isRed(h.left.left)) {
            h = rotateRight(h);
        }
        if (isRed(h.left) && isRed(h.right)) {
            flipColors(h);
        }
        return h;
    }
    //删除最大值
    public void deleteMax() {
        if (!isRed(root.left) && !isRed(root.right)) {
            root.color = RED;
        }
        root = deleteMax(root);
        if (root == null) {
            root.color = BLACK;
        }
    }
    private Node deleteMax(Node node) {
        if (node.right == null) {
            return null;
        }
        if (!isRed(node.right) && !isRed(node.right.left)) {
            node = moveRedRight(node);
        }
        node.right = deleteMax(node.right);
        return balance(node);
    }
    //查找key的值
    public Value search(Key key) {
        Node node = search(root, key);
        return node == null ? null : node.value;
    }
    private Node search(Node node, Key key) {
        if (node == null || node.key == key) {
            return node;
        }
        int cmp = key.compareTo(node.key);
        if (cmp < 0) {
            return search(node.left, key);
        } else {
            return search(node.right, key);
        }
    }
    private Node moveRedRight(Node h) {
        //假设结点h为红色,h.left和h.right.left都是黑色
        //将h.right或者h.right的子节点之一变为红色
        flipColors(h);
        if (isRed(h.left.left)) {
            h = rotateRight(h);
            flipColors(h);
        }
        return h;
    }
    //删除key操作
    public void delete(Key key) {
        if (root == null) {
            return;
        }
        root = deleteNode(root, key);
        root.color = BLACK;
    }
    private Node deleteNode(Node node, Key key) {
        if (node == null) {
            return null;
        }
        int cmp = key.compareTo(node.key);
        if (cmp < 0) {
            node.left = deleteNode(node.left, key);
        } else if (cmp > 0) {
            node.right = deleteNode(node.right, key);
        } else {
            if (node.left == null) {
                return node.right;
            } else if (node.right == null) {
                return node.left;
            }
            node.key = (Key) minValue(node.right);
            node.right = deleteNode(node.right, node.key);
        }
        if (isRed(node.right) && !isRed(node.left)) {
            node = rotateLeft(node);
        }
        if (isRed(node.left) && isRed(node.left.left)) {
            node = rotateRight(node);
        }
        if (isRed(node.left) && isRed(node.right)) {
            flipColors(node);
        }
        return node;
    }
    private Key minValue(Node node) {
        Key minValue = node.key;
        while (node.left != null) {
            minValue = (Key) node.left.key;
            node = node.left;
        }
        return minValue;
    }
}

1.2.3 多向平衡二叉树(B-树)

        B-树是一颗由键的副本组成的树,每个副本都关联着一条链接。使得索引和符号表分开。更适合大规模的数据存储、高访问速度或随机访问的场景,后被B+树(适合范围查询和有序遍历的场景)优化。

//B-树集合的实现
public class BTreeSET<Key extends Comparable<Key>{
    public class Page<Key>{
        //创建并打开一个页
        public Page(boolean bottom){}
        //关闭页
        public void close(){}
        //将键查放入外部的页中
        public void add(Key key){}
        //打开p,向这个内部页中插入一个条目并将p和q中的最小键相关联
        public void add(Page page){}
        //这是一个外部页吗
        public boolean isExternal(){
            return true;
        }
        //键key在页中吗
        public boolean contains(Key key){
            return true;
        }
        //可能含有键key的子树
        public Page next(Key key){
            return new Page(true);
        }
        //页是否已经溢出
        public boolean isFull(){
            return false;
        }
        //将较大的中间键移动到一个新的页中
        public Page split(){
            return new Page(true);
        }
        //页中所有键
        public Iterable<Key> keys(){
            return new Iterable<Key>() {
                @Override
                public Iterator<Key> iterator() {
                    return null;
                }
            };
        }
    }
    private Page root = new Page(true);
    public BTreeSET(Key sentinel){
        add(sentinel);
    }
    public void add(Key key){
        add(root,key);
        if (root.isFull()){
            Page lefthalf =root;
            Page righthalf=root.split();
            root = new Page(false);
            root.add(lefthalf);
            root.add(righthalf);
        }
    }
    public void add(Page h,Key key){
        if (h.isExternal()) {
            h.add(key);
            return;
        }
        Page next = h.next(key);
        add(next,key);
        if (next.isFull()){
            h.add(next.split());
        }
        next.close();
    }
    public boolean contains(Key key){
        return contains(root,key);
    }
    private boolean contains(Page h,Key key){
        if (h.isExternal()){
            return h.contains(key);
        }
        return contains(h.next(key),key);
    }
}
B-树在查找时使用Page数据类型来将键和可能含有该键的子树相关联,并通过检测键的溢出和分裂结点的方法完成了插入操作。

1.3 基于散列表实现的符号表/索引表

        如果所有的键都是小整数,我们就可以使用数组来实现无序的符号表,将键作为索引,这样就可以快速访问到键的值,这样就是我们要基于散列表来实现符号表。

        ①用散列函数将键转化为数组的一个索引。

        ②处理不同键有相同的索引及处理碰撞冲突。【使用拉链法和线性探测法来解决】

使用散列表可以在时间复杂度为常数级别的查找和插入操作。

要实现一个优秀的散列方法需要满足三个条件:

        ①一致性---等价的键必然产生相等的散列值。

        ②高效性---计算简便。

        ③均匀性---均匀地散列所有的键。

java通常直接使用对象的hashcode()函数来实现散列即可。

1.3.1 基于拉链法的散列表

        拉链法来解决碰撞冲突,即将大小为M的数组中的每个元素指向一条链表,链表中的每个结点都存储了该元素的键值对。在键的顺序不重要的应用中,它可能是最快的。

//基于拉链法的散列表
public class ChainingHashST<Key, Value> {
    private int N;//键值对总数
    private int M;//散列表大小
    private ChainingHashST<Key, Value>[] st;//存放链表对象的数组
    public ChainingHashST() {
        this(997);
    }
    public ChainingHashST(int M) {
        //创建M条链表,默认创建997条
        this.M = M;
        st = new ChainingHashST[M];
        for (int i = 0; i < M; i++) {
            st[i] = new ChainingHashST();
        }
    }
    private int hash(Key key){
        return (key.hashCode() & 0x9fffffff) % M;
    }
    private Value get(Key key){
        return st[hash(key)].get(key);
    }
    public void put(Key key,Value value){
        st[hash(key)].put(key,value);
    }
}

1.3.2 基于线性探测法的散列表(开发地址散列表)

        线性探测法是用大小为M的数组保存N个键值对【M>N】,需要依靠数组中的空位解决碰撞冲突。但碰撞发生时,直接检查散列表中的下一位置(将索引值加1),直到下一位置不发生碰撞位置。使用并行数组来实现,一条保存键,一条保存值。

//基于线性探测法的散列表(开放寻找法)
public class LinearHashST<Key, Value> {
    private int N;//符号表中键值对总数
    private int M;//线性探测表的大小
    private Key[] keys;//键
    private Value[] values;//值
    public LinearHashST() {
        keys = (Key[]) new Object[M];
        values = (Value[]) new Object[M];
    }
    public LinearHashST(int cap) {
        keys = (Key[]) new Object[cap];
        values = (Value[]) new Object[cap];
    }
    private int hash(Key key) {
        return (key.hashCode() & 0x7fffffff) % M;
    }
    public void put(Key key, Value value) {
        if (N >= M / 2) {
            //扩大线性探测表M=2M;
            resize(2 * M);
        }
        int i;
        for (i = hash(key); keys[i] != null; i = (i + 1) % M) {
            if (keys[i].equals(key)) {
                values[i] = value;
                return;
            }
        }
        keys[i] = key;
        values[i] = value;
        N++;
    }
    public Value get(Key key) {
        for (int i = hash(key); keys[i] != null; i = (i + 1) % M) {
            if (keys[i].equals(key)) {
                return values[i];
            }
        }
        return null;
    }
    public void delete(Key key) {
        //删除键的同时,需要该键的右侧的所有键重新插入散列表
        if (!contain(key)) {
            return;
        }
        int i = hash(key);
        while (!key.equals(keys[i])) {
            i = (i + 1) % M;
        }
        keys[i] = null;
        values[i] = null;
        i = (i + 1) % M;
        while (keys[i] != null) {
            Key keyToRedo = keys[i];
            Value valueToRedo = values[i];
            keys[i] = null;
            values[i] = null;
            N--;
            put(keyToRedo, valueToRedo);
            i = (i + 1) % M;
        }
        N--;
        //散列表的性能依赖a=N/M即散列表的使用率,
        // 对于拉链法。使用率>1,对于线性探测法不大于1,最好在1/8到1/2
        if (N > 0 && N == M / 8) {
            //缩小线性探测表的大小M = M/2;
            resize(M / 2);
        }
    }
    private boolean contain(Key key) {
        for (Key key1 : keys) {
            if (key1.equals(key)) {
                return true;
            }
        }
        return false;
    }
    //扩大或者缩小数组
    private void resize(int cap) {
        LinearHashST<Key, Value> t = new LinearHashST<Key, Value>(cap);
        for (int i = 0; i < M; i++) {
            if (keys[i] != null) {
                t.put(keys[i], values[i]);
            }
        }
        keys = t.keys;
        values = t.values;
        M = t.M;
    }
}

2. 查找算法总结

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值