基于有序数组的符号表查找(二分法)
相比于无序链表的符号表顺序查找,使用基于有序数组的符号表二分法查找可以根据索引,有效减少每次查找所需比较次数,因为每次都可以快速找到数组的中间元素,而如果想通过无序链表找到中间节点的数据,唯一的方法就是遍历链表。
在开始前,需要明确的是,我们要维护两个数组,使他们作为实现本次符号表查找的数据结构。这两个数组分别是key(键)数组和value(值)数组,并且保证他们有序。本篇在类模版的实现中,将其写成私有变量。
vector<KEY> m_key;
vector<VALUE> m_value;
为简化代码(懒),本篇省去写对两个数组进行排序的构造函数。测试时,直接输入两个有序数组即可,支持动态扩充数组。
一.符号表查找的基本方法
int SymbleTable_size(); //返回有序数组大小
int rank_normal(KEY key);//返回键有序数组中,小于指定键的键的数量
VALUE get(KEY key);//根据指定键,返回该键对应的值
void put(KEY key, VALUE value);//输入新的键值对,分别插入到两个有序数组当中
KEY min();//返回键数组中最小的键
KEY max();//返回键数组中最大的键
KEY select(int k);// 获得排名为K的键
KEY ceiling(KEY key);// 大于等于key键的最小键
KEY floor(KEY key);// 小于等于key键的最大键
bool isEmpty();// 判断有序数组是否为空
void deleteKey(KEY key);//删除某个键值对,其键指定为key
void print_as();//输出键值对内容
其中KEY 和 VALUE 是类模版的类型参数,分别表示键的数据类型和值的数据类型,本次测试时规定key是int类型,value是 float 类型,如下所示。
vector<int> key{1,3,5,7,9,11,13,15};
vector<float> value{1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8};
1.rank()
在这些方法中,rank()
是核心方法,它几乎支持了以上所有的方法实现。下面我们先来梳理rank()的具体作用。从内容上来说,rank()
是一个二分法查找数组中指定元素并返回其索引的方法,但它在本篇中,保持两个特性
1.如果键数组中,存在我们所输入的指定的键,那么就返回在数组中比指定键小的键的数量,比如我们键数组为[1,2,3,4,5]
,我们想要查找键 5
,令int i = rank(5)
,i的值就是4,4既是比键5小的键的数量,还是查找指定键的索引,所以 m_key[i] == key
(key为指定键)可以判断键数组中是否存在指定的键,如存在,可以继续进行其他操作,比如更新键值对,
2.如果键数组中,并不存在指定的键,那么还是要返回比这个键小的键的数量,这个特性可以使得插入新键值对方法顺利完成,或者在删除键值对以及根据指定键获得值的方法中直接返回等。
基于以上对rank的期待,让我们实现这个rank吧,有两种方法可以实现,分别是迭代和递归,两者实现的功能等价。
rank()
的迭代版本实现如下所示
template<typename KEY, typename VALUE> int ArraySearch<KEY, VALUE>::rank_normal(KEY key)
{
lo = 0;
hi = SymbolTable_size()-1;
while(lo <= hi)
{
int mid = lo + (hi-lo)/2;
if(key > m_key[mid]) lo = mid + 1;
else if(key < m_key[mid]) hi = mid-1;
else return mid;
}
return lo;
}
若最终返回的lo
,该方法虽然没有找到指定的键,但还是返回了lo
作为比指定键小的键的数量,大家可以通过一个[1,2,3,4]
这样一个简单的键数组验证一下。lo
和 hi
会在该方法执行的过程中逼近指定键,若没有找到指定键,最终lo
会变成我们所要的结果,下面给出递归方法。
template<typename KEY, typename VALUE> int ArraySearch<KEY, VALUE>::rank_iter(KEY key, int lo, int hi)
{
if(lo > hi) return lo;
mid = lo + (hi - lo)/2
if(key > m_key[mid]) return rank_iter(KEY key, mid+1, hi);
else if(key < m_key[mid]) return rank_iter(KEY key, lo, mid-1);
else return mid;
}
如上所述,迭代法和递归法实现功能相同。接下来就可以基于rank()
来实现其他的方法了。
2.put
当用户需要插入一个新的键值对,或者通过一个已有的key更新这个key的value时,put()函数可以解决这两个问题。
大概的算法流程是
首先获得数组中小于等于指定键的键的数量i
(1)如果不存在指定键,那么就在i
后面的位置添加一个指定键,并在值数组相应位置添加新的值(实现此方法需要i
之后的数据整体往后推一个位置,代价较高,因此如果某个符号表数据量很大并且需要经常插入新的键值对,那么这个有序数组的二分法查找就不怎么适用了)
(2)如果存在指定键,那就更新该键的值(通过i
更新)
template<typename KEY, typename VALUE> void ArraySearch<KEY, VALUE>::put(KEY key, VALUE value)
{
int i = rank_normal(key);
if(m_key[i] == key)
{
m_value[i] = value;
}
else
{
m_key.resize(m_key.size()+1);
m_value.resize(m_value.size()+1);
for(j = m_key.size();j > i; --j)
{
m_key[j] = m_key[j-1];
m_value[j] = m_value[j-1];
}
m_key[i] = key;
m_value[i] = value;
}
}
3.get
get()
方法用于通过给定键,获得其value,同样需要rank
方法的支持,这里需要通过获得rank
返回值来确定指定键是否存在于键数组当中,实现较简单,代码如下所示。
template<typename KEY, typename VALUE> VALUE ArraySearch<KEY, VALUE>::get(KEY key)
{
int i = rank_normal(key);
if(isEmpty())
{
cout << "符号表为空" << endl;
return m_value[0];
}
if(m_key[i] == key) return m_value[i];
else
{
cout << "未找到该键" << endl;
return m_value[0];
}
}
4.delete
delete
方法同样需要移动部分数组,来保证代表键值对的两个数组大小减1。
大致算法流程如下:
1.如果键数组为空,则返回
2.通过rank获取小于指定键的键的数量i
,并通过i
判断指定键是否在键数组当中。
(1)如果存在,直接将i
位置后面的所有键值对数据向前移动一个位置,然后重新规定两个平行数组大小。
(2)如果不存在,就返回。
template<typename KEY, typename VALUE> void ArraySearch<KEY, VALUE>::deleteKey(KEY key)
{
if(isEmpty())
{
cout << "nothing could be deleted" << endl;
return;
}
int i = rank_normal(key);
if(m_key[i] != key)
return;
for(int j = i; j < m_key.size(); j++)
{
m_key[j] = m_key[j + 1];
m_value[j] = m_value[j + 1];
}
m_key.resize(m_key.size()-1);
m_value.resize(m_value.size()-1);
print_as();
}
4.其他方法
其他方法较简单,不多赘述。
5.完整代码
//
// The_symbol_table.hpp
// Dat_Structure
//
// Created by 云子谣 on 2020/2/27.
// Copyright © 2020 云子谣. All rights reserved.
//
#ifndef The_symbol_table_hpp
#define The_symbol_table_hpp
#include <stdio.h>
#include <vector>
#include <iostream>
#endif /* The_symbol_table_hpp */
using namespace std;
template<typename KEY,typename VALUE> class ArraySearch
{
public:
ArraySearch(vector<KEY> kk, vector<VALUE> vv);
~ArraySearch();
int Array_size();
int rank_normal(KEY key);
VALUE get(KEY key);
void put(KEY key, VALUE value);
KEY min();
KEY max();
KEY select(int k);
KEY ceiling(KEY key);
KEY floor(KEY key);
bool isEmpty();
void deleteKey(KEY key);
void print_as();
private:
vector<KEY> m_key;
vector<VALUE> m_value;
};
template<typename KEY, typename VALUE> ArraySearch<KEY, VALUE>::ArraySearch(vector<KEY> kk, vector<VALUE> vv)
{
m_key= kk;
m_value = vv;
}
template<typename KEY, typename VALUE> ArraySearch<KEY, VALUE>::~ArraySearch(){}
template<typename KEY, typename VALUE> int ArraySearch<KEY, VALUE>::Array_size()
{
return m_key.size();
}
template<typename KEY, typename VALUE> bool ArraySearch<KEY, VALUE>::isEmpty()
{
return (Array_size() == 0);
}
template<typename KEY, typename VALUE> int ArraySearch<KEY, VALUE>::rank_normal(KEY key)
{
int lo = 0, hi = Array_size()-1;
//假如key数组为[1,2,3,4,5,6],lo = 0 指第一个元素索引,hi为该数组最后一个元素的索引5
while(lo <= hi)
{
int mid = lo + (hi-lo)/2;
if(key > m_key[mid]) lo = mid + 1;
else if(key < m_key[mid]) hi = mid-1;
else return mid;
}
return lo;//此时如果查找的数据存在,mid就是所查找的数据, 但此时应返回比查找的数组小的数据数量,在二分法算法执行的过程中,lo和hi会慢慢逼近mid,即会慢慢逼近要查找的key,所以lo最终会变成离key最近但比key小的索引
}
template<typename KEY, typename VALUE> void ArraySearch<KEY, VALUE>::put(KEY key, VALUE value)
{
int i = rank_normal(key);
if(i <= m_key.size() && m_key[i] == key)
{
//m_value.resize();
//m_value[i] = value;
//此时,i虽然代表了比key小的键值的个数,但同时,m_key[i]也代表了所要检索的键,
//m_key[i] == key,判断 想要更新的键值对是否在表中
//若不在需要重新插入å
m_value[i] = value;
return;
}
m_value.resize(m_value.size()+1);
m_key.resize(m_key.size()+1);
for(int j = m_key.size(); j > i; --j)
{
m_key[j] = m_key[j - 1];
m_value[j] = m_value[j - 1];
}
m_key[i] = key;
m_value[i] = value;
}
template<typename KEY, typename VALUE> VALUE ArraySearch<KEY, VALUE>::get(KEY key)
{
if(isEmpty())
{
cout << "符号表为空" << endl;
return m_value[0];
}
int i = rank_normal(key);
if(i <= m_key.size() && m_key[i] == key)
{
return m_value[i];
}
else
{
cout << "未找到该键" << endl;
return m_value[0];
}
}
template<typename KEY, typename VALUE> void ArraySearch<KEY, VALUE>::print_as()
{
typename vector<KEY>::iterator iter_k;
typename vector<VALUE>::iterator iter_v;
cout << "-------display key--------" << endl;
for(iter_k = m_key.begin(); iter_k != m_key.end(); iter_k++)
{
cout << *(iter_k) << endl;
}
cout << "-------display VALUE--------" << endl;
for(iter_v = m_value.begin();iter_v != m_value.end(); iter_v++)
{
cout << *(iter_v) << endl;
}
}
template<typename KEY, typename VALUE> KEY ArraySearch<KEY, VALUE>::min()
{
return m_key[0];
}
template<typename KEY, typename VALUE> KEY ArraySearch<KEY, VALUE>::max()
{
return *(m_key.end()-1);
}
template<typename KEY, typename VALUE> KEY ArraySearch<KEY, VALUE>::select(int k)
{
return m_key[k];
}
template<typename KEY, typename VALUE> KEY ArraySearch<KEY, VALUE>::ceiling(KEY key)
{
int i = rank_normal(key);
return m_key[i];
}
template<typename KEY, typename VALUE> KEY ArraySearch<KEY, VALUE>::floor(KEY key)
{
int i = rank_normal(key);
if(m_key[i] == key)
{
return m_key[i];
}
else
{
return m_key[i-1];
}
}
template<typename KEY, typename VALUE> void ArraySearch<KEY, VALUE>::deleteKey(KEY key)
{
if(isEmpty())
{
cout << "nothing could be deleted" << endl;
return;
}
int i = rank_normal(key);
if(m_key[i] != key)
return;
for(int j = i; j < m_key.size(); ++j)
{
m_key[j] = m_key[j+1];
m_value[j] = m_key[j+1];
}
m_key.resize(m_key.size()-1);
m_value.resize(m_value.size()-1);
print_as();
}
6.二分查找分析
在N个键的有序数组中进行的二分查找最多需要(lgN+1)次比较,个人理解是虽然二分查找速度很快,但在处理插入删除问题时,没有链表的灵活性高。
下一篇会实现一个二叉查找树,它即有链表的灵活性,也有二分查找的效率。
参考资料:《算法》第四版