查找算法总结(顺序查找、二分查找、二叉树、平衡二叉树、红黑树、散列表hash)

符号表查找

以键值对进行存储,每个键对应一个不重复的值。

关键函数:put(key, value)、get(key)、delete(key)、contains(key)

常用的数据结构

一、链表

每个节点存储key、value、next;get的实现为遍历链表并找到相同的键;put的实现为遍历链表判断是否有相同的键,如果有则更新值,否则在链表头新增节点。

优点:适用于小问题

缺点:大型数据查找较慢

二、有序数组

使用一对平行数组,一个存储键一个存储值,保证键有序从而使用二分查找来实现get。这里引入一个rank函数,能够返回表中小于给定键的键的数量,这样get函数就能够通过rank返回对应键的下标,从而在值的数组中根据下标找到对应值;put函数能够使用rank找到存入的键所需放置的位置,再更新移动数组,将新的键值对插入。

//递归实现rank
int rank(int key, int lo, int hi){
    if(hi < lo) return lo;
    int mid = (lo + hi)/2;
    if(key < a[mid])
        return rank(key, lo, mid);
    else if (key > a[mid])
        return rank(key, mid, hi);
    else
        return mid;
}
//迭代实现rank
int rank(int key){
    int lo = 0, hi = N-1;
    while(lo <= hi){
        int mid = (lo + hi)/2;
        if (key < a[mid])
            hi = mid -1;
        else if (key > a[mid])
            lo = mid +1;
        else 
            return mid;
    }
    return lo;
}

int get(int key){
    int i = rank(key);
    if(i < N && keys[i] == key)
        return vals[i];
    else
        return null;
}

void put(int key, int value){
    int i = rank(key);
    if(i < N && keys[i]==key){
        vals[i] = value;
        return;
    }
    for(int j=N; j>i; j--){
        keys[j] = keys[j-1];
        vals[j] = vals[j-1];
    }
    keys[i] = key;
    vals[i] = val;
    N++;
}

优点:最优的查找效率和空间需求;能够进行有序性相关操作

缺点:插入操作很慢

三、二叉树

结合了链表插入的灵活性和有序数组查找的高效性。

每一个结点包含:一个结点计数器(以该结点为根的子树中的结点总数),一个左结点,一个右结点,一个键,一个值。每个结点的键均大于其左子树上的键而小于其右子树上的键。

int get(Node x, int key){
    if(x == null)
        return null;
    if(key < x.key)
        return get(x.left, key);
    else if (key > x.key)
        return get(x.right, key);
    else
        return x.value;
}

void put(int key, int val){
    root = put(root, key, val);
}

Node put(Node x, int key, int val){
    if (x == null)
        return new Node(key, val, 1);//最后一个参数为结点计数器
    if(key < x.key)
        x.left = put(x.left, key, val);
    else if (key > x.key)
        x.right = put(x.right, key, val);
    else
        x.value = val;
    x.N = size(x.left) + size(x.right) + 1;
    return x;
}

运行时间取决于树的形状,树的形状取决于键被插入的先后顺序,最好情况是平衡树,最坏情况是线性树。

删除树的最小结点也就是删除树中最左侧的结点,因为结点的大小顺序为:左<中<右

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;
}

那如果我们需要删除任意一个指定结点,在进行删除结点操作时,可以用它的后继结点填补,也就是用右子树的最小结点填补。

Node delete(Node x, int key){
    if(x == null)
        return null;
    if(key < x.key)
        x.left = delete(x.left, key);
    else if (key > x.key)
        x.right = delete(x.right, key);
    else{
        if(x.right == null)
            return x.left;
        if (x.left == null)
            return x.right;
        //找到右子树上的最小结点,将其填补到当前位置,其左子树就是原有左子树,右子树就是右子树删除最小点后的子树
        // 这里的填补是一个递归操作,将填补的结点x返回了,作为原有结点的父节点的子节点
        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-3查找树

3-结点含有两个键和三个结点,设两个键为S和T(S<T),则左结点均小于S,中结点介于S和T之间,右结点大于T。完美的2-3查找树中所有的空链接应该在同一层中。


2-3查找树的查找和二叉树非常类似,判断键所在区间,不断向下查找即可。这里需要关注的是2-3查找树的插入操作,如何插入能够一直保证树的平衡:

1) 向2-结点插入:将2-结点变换为3-结点即可;

2) 向一颗只含有一个3-结点的树插入:将3-结点变为4-结点,然后将其分解为2-3树,树高增加一层;

3) 向一个父结点为2-结点的3-结点插入:将3-结点变为4-结点,再将其中键提取至父结点,将父结点变为一个3-结点;

4) 向一个父结点为3-结点的3-结点插入:按照3) 中的方法不断向上替换直至遇到一个2-结点,如果根结点也是3-结点则使用2) 中的方法将树高增加一层。

2、红黑二叉查找树

将两个2-结点使用红链接连接起来构成一个3-结点,而普通链接则由黑链接表示,也就是说用左斜的红色链接表示3-结点,从而使树看起来和二叉树结构相同,便于查找。  黑链上是平衡的。

2-结点进行插入:

都是将新的结点的链接设为红链接,如果位于右侧的话则通过旋转将其旋转至左侧。

一棵双键树(一个3-结点) S-T进行插入:

如果新键最大,则置于T右侧,将其父结点T的两个子结点链接均变为黑色;如果新键a最小,则先通过旋转变成a-S-T,S-T为红色右链接,再将链接均变为黑色;如果新键位于S和T两者之间,则将其置于S的右链接(红),将红色右链接S-a旋转变为红色左连接(S为a的左结点),再旋转S-a-T(同位于左侧)旋转变成红色右链接(S-a-T以a为中心根结点,两侧都是红链接),再将链接变成黑色。   

在将两个红色子链接变成黑色时,需要将父结点自己的颜色变成红色;红黑树的根结点一定是黑色,如果是红色需要修改为黑色,同时树高度加一。

void put(int key, int val){
    root = put(root, key, val);
    root.color = BLACK;
}
Node put(Node h, int key, int val){
    if (h == null)
        return new Node(key, val, 1, RED);
    if (key < h.left)
        h.left = put(h.left, key, val);
    else if (key > h.right)
        h.right = put(h.right, key, val);
    else
        h.val = val;

    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;
}
//右链接为红时,向左旋转
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;
}
//左链接有连续两个红链接时,向右旋转
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;
}
//两个子链接都是红色时,变换颜色
void flipColors(Node h){
    h.color = RED;
    h.left.color = BLACK;
    h.right.color = BLACK;
}

删除操作?


五、散列表hash

查找:1) 用散列函数将被查找的键转化为数组的一个索引; 2) 处理碰撞冲突 (拉链法、线性探测法)

散列函数

散列函数需要保证能够将任意键转化为数组范围内的索引,同时需要易于计算且均匀分布所有键。

常用方法:除留余数法k%M

碰撞冲突处理

1) 拉链法

将数组中的每个元素都指向一条链表,链表的每个结点都存储散列值为该元素索引的键值对。Java中链表表示为一个SequentialSearchST()的对象,可以直接进行put、get和delete操作。

2) 线性探测法

使用大小为M的数组保存N个键值对,M>N,通过数组中的空位解决碰撞冲突。也就是说,当碰撞发生时 (一个键的散列值已经被另一个不同的键占用),我们直接检查散列表中的下个位置直至找到相应的键。

void put(int key, int val){
    if(N>=M/2)
        resize(2*M);//将数组扩大
    int i;
    for(i=hash(key); keys[i]!=null; i=(i+1)%M)
        if(keys[i].equals(key)){
            vals[i] = val;
            return;
        }
    keys[i] = key;
    vals[i] = val;
    N++;
}
int get(int key){
    for(int i=hash(key); keys[i] != null; i=(i+1)%M)
        if(keys[i].equals(key))
            return vals[i];
    return null;
}

线性探测法的删除操作比较复杂,不能直接将对应位置置为null,这会使得之后的键值无法被查到。我们需要将被删除键右侧的所有键重新插入散列表。

void delete(int key){
    if(!contains(key))  return;
    int i = hash(key);
    while(!(key == keys[i]))
        i = (i+1)%M;
    keys[i] = null;
    vals[i] = null;
    i = (i+1)%M;
    while(keys[i] != null){
        int keyToRedo = keys[i];
        int valToRedo = vals[i];
        keys[i] = null;
        vals[i] = null;
        N--;
        put(keyToRedo, valToRedo);
        i = (i+1)%M;
    }
    N--;
    if(N>0 && N == M/8)
        resize(M/2);
}

优点:能够快速查找和插入常见类型的数据

缺点:需要计算每种类型的数据的散列;无法进行有序性操作;链接和空节点需要额外空间



©️2020 CSDN 皮肤主题: 技术黑板 设计师:CSDN官方博客 返回首页