您的位置:首页 > 其它

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

2018-03-11 22:47 639 查看

符号表查找

以键值对进行存储,每个键对应一个不重复的值。
关键函数: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);
}
优点:能够快速查找和插入常见类型的数据
缺点:需要计算每种类型的数据的散列;无法进行有序性操作;链接和空节点需要额外空间
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息