1 常见查找概念
查找指根据给定的某个值,在查找表中确定一个其关键字等于给定值的数据元素。 查找表是由同一类型的数据元素或记录构成的集合。 关键字是数据元素中某个数据项的值,又称为键值,主关键字可以唯一地标识一个记录 ,次关键字可以标识多个数据元素或记录。 静态查找表:只做查找操作的查找表,可用线性表结构来组织数据。 动态查找表:在查找的过程中同时插入查找表中不存在的数据元素,或者从查找表中删除已经存在的某个数据元素。可以用二叉排序树来组织数据。 从逻辑上来说,查找所基于的数据结构是集合,集合中的记录之间没有本质关系。若想获得较高的查找性能,我们可以改变数据元素之间的关系,在存储时将查找集合组织成表、树等结构。
2 顺序表查找算法
是最基本的查找技术,适用于小型数据的查找。可以将容易被查找到的记录放在前面以提高效率。 时间复杂度为O(n)
bool sequentialSearch1 ( const int * a, int n, int key, int & pos)
{
for ( int i = 0 ; i < n; ++ i)
{
if ( a[ i] == key)
{
pos = i;
return true ;
}
}
return false ;
}
bool sequentialSearch2 ( const int * a, int n, int key, int & pos)
{
int i = n;
while ( a[ i] != key)
-- i;
if ( i == 0 )
return false ;
else
{
pos = i;
return true ;
}
}
3 有序表查找
3.1 折半查找
又称为二分查找,记录必须关键字有序并且采用顺序存储,适用于静态查找表 ,对于需要频繁执行插入或删除操作的数据集来说,维护有序的排序会带来较大的工作量,不适宜使用。 时间复杂度为O(logn)
bool binarySearch ( const int * a, int n, int key, int & pos)
{
int low = 0 ;
int high = n - 1 ;
while ( low <= high)
{
int mid = ( low + high) / 2 ;
if ( key < a[ mid] )
high = mid - 1 ;
else if ( key > a[ mid] )
low = mid + 1 ;
else
{
pos = mid;
return true ;
}
}
return false ;
}
3.2 插值查找
基本思想是根据要查找的关键字key与查找表中最大最小记录的关键字比较后的查找方法。 对于表长较大,而关键字分布又比较均匀的查找表来说,插值查找算法的平均性能比折半查找要好得多。
bool insertSearch ( const int * a, int n, int key, int & pos)
{
int low = 0 ;
int high = n - 1 ;
while ( low <= high)
{
int mid = low + ( high - low) * ( key - a[ low] ) / ( a[ high] - a[ low] ) ;
if ( key < a[ mid] )
high = mid - 1 ;
else if ( key > a[ mid] )
low = mid + 1 ;
else
{
pos = mid;
return true ;
}
}
return false ;
}
3.3 斐波那契查找
利用了黄金分割的原理实现,平均性能优于折半查找 三种有序表查找本质上是分割点选择的不同,就平均性能来说,斐波那契查找优于折半查找。
bool fibonacciSearch ( const int * a, int n, int key, int & pos)
{
int low = 0 ;
int high = n - 1 ;
int k = 0 ;
while ( n > F[ k] - 1 )
++ k;
for ( int i = n; i < F[ k] - 1 ; ++ i)
a[ i] = a[ n] ;
}
4 二叉排序树BST
二叉排序树的插入和删除效率不错,又可以比较高效率地实现查找算法,用于动态查找表中。 二叉排序树又被称为二叉查找树,它或者是一颗空树,或者是具有下列性质的二叉树:若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;它的左右子树也分别为二叉排序树。 构造二叉排序树的目的不是为了排序,而是为了提高查找和插入删除关键字的速度。 二叉排序树的查找走的就是从根节点到要查找结点的路径,比较次数等于给定值结点在二叉排序树的深度,即二叉排序树的查找性能取决于二叉排序树的形状。对于递增的数组,二叉排序树退化为单链表,使层数迅速增加,我们希望二叉排序树是平衡的,深度与完全二叉树相同,为[log2n] + 1
typedef struct _BiTNode
{
int data;
struct BiTNode * lchild;
struct BiTNode * rchild;
} BiTNode, * BiTree;
bool searchBst ( const BiTree T, int key, BiTree f, BiTree* p)
{
if ( T == nullptr )
{
* p = f;
return false ;
}
else if ( key == T-> data)
{
* p = T
return true ;
}
else if ( key < T-> data)
return searchBst ( T-> lchild, key, T, p) ;
else
return searchBst ( T-> rchild, key, T, p) ;
}
bool insertBst ( BiTree* T, int key)
{
BiTNode* p = nullptr ;
if ( ! searchBst ( * T, key, nullptr , & p) )
{
BiTNode* s = new BiTNode;
s-> data = key;
s-> lchild = s-> rchild = nullptr ;
if ( p == nullptr )
* T = s;
else if ( key < p-> data)
p-> rchild = s;
else
p-> lchild = s;
return true ;
}
return false ;
}
bool deleteBst ( BiTree* T, int key)
{
if ( T == nullptr || * T == nullptr )
return false ;
else
{
if ( key == ( * T) -> data)
return deleteNode ( T) ;
else if ( key < ( * T) -> data)
return deleteBst ( & ( * T) -> lchild, key) ;
else
return deleteBst ( & ( * T) -> rchild, key) ;
}
}
bool deleteNode ( BiTree* p)
{
BiTNode* q = nullptr ;
BiTNode* s = nullptr ;
if ( ( * p) -> rchild == nullptr )
{
q = * p;
* p = ( * p) -> lchild;
free ( q) ;
}
else if ( ( * p) -> lchild == nullptr )
{
q = * p;
* p = ( * p) -> rchild;
free ( q) ;
}
else
{
q = * p;
s = ( * p) -> lchild;
while ( s-> rchild)
{
q = s;
s = s-> rchild;
}
( * p) -> data = s-> data;
if ( q != * p)
q-> rchild = s-> lchild;
else
q-> lchild = s-> lchild;
free ( s) ;
}
return true ;
}
5 平衡二叉树
对于动态查找来说,我们在频繁查找的同时也需要经常插入和删除结点。在构建二叉排序树时如果该树是不平衡的,那么查找效率就会很低,因此我们在构建时就让这棵二叉树是平衡的。 平衡二叉树是一种特殊的二叉排序树,每一个节点的左子树和右子树的高度差至多为1。 在平衡二叉树中进行查找、插入、删除的时间复杂度为O(logn) 。 平衡因子定义为二叉树上结点左子树深度减去右子树深度的值 ,平衡二叉树所有结点的平衡因子只可能为-1,0,1。
5.1 AVL树
最小不平衡子树 定义为距离插入结点最近的,且平衡因子绝对值大于1的结点为根的子树。AVL树是一种平衡二叉树,构建的基本思想是将不平衡消灭在最早时刻。每当插入一个结点时,先检查是否因插入而破坏了树的平衡性,若是,则找出最小不平衡子树。在保持BST特性的前提下,调整最小不平衡子树中各结点之间的链接关系,进行相应的旋转,使之成为新的平衡子树。
typedef struct _BiTNode
{
int data;
int bf;
struct BiTNode * lchild, * rchild;
} BiTNode, * BiTree;
void rightRotate ( BiTree* p)
{
BiTNode* L;
L = ( * p) -> lchild;
( * P) -> lchild = L-> rchild;
L-> rchild = * p;
* p = L;
}
void leftRotate ( BiTree* p)
{
BiTNode* R;
R = ( * p) -> rchild;
( * p) -> rchild = R-> lchild;
R-> lchild = * p;
* p = R;
}
# define LH 1
# define RH - 1
# define EH 0
void leftBalance ( BiTree* T)
{
BiTNode* L = ( * T) -> lchild;
BiTNode* Lr;
switch ( L-> bf)
{
case LH:
( * T) -> bf = L-> bf = EH;
rightRotate ( T) ;
break ;
case RH:
Lr = L-> rchild;
switch ( Lr-> bf)
{
case LH:
( * T) -> bf = RH;
L-> bf = EH;
break ;
case EH:
( * T) -> bf = L-> bf = EH;
break ;
case RH:
( * T) -> bf = EH;
L-> bf = LH;
break ;
}
Lr-> bf = EH;
leftRotate ( ( * T) -> lchild) ;
rightRotate ( T) ;
}
}
void rightBalance ( BiTree* T)
{
BiTNode* R = ( * T) -> rchild;
BiTNode* Rl;
switch ( R-> bf)
{
case RH:
( * T) -> bf = ( * R) -> bf = EH;
leftRotate ( T) ;
break ;
case LH:
Rl = R-> lchild;
switch ( Rl-> bf)
{
case LH:
( * T) -> bf = RH;
R-> bf = EH;
break ;
case EH:
( * T) -> bf = R-> bf = EH;
break ;
case RH:
( * T) -> bf = EH;
R-> bf = LH;
break ;
}
Rl-> bf = EH;
rightRotate ( & ( * T) -> rchild) ;
leftRotate ( T) ;
}
}
bool insertAVL ( BiTree* T, int e, bool * taller)
{
if ( * T == nullptr )
{
* T = ( BiTNode* ) malloc ( sizeof ( BiTNode) ) ;
( * T) -> data = e;
( * T) - lchild = ( * T) -> rchild = nullptr ;
( * T) -> bf = EH;
* taller = true ;
}
else
{
if ( e == ( * T) -> data)
{
* taller = true ;
return false ;
}
if ( e < ( * T) -> data)
{
if ( ! insertAVL ( & ( * T) -> lchild, e, taller) )
return false ;
if ( * taller)
{
switch ( ( * T) -> bf)
{
case LH:
leftBalance ( T) ;
* taller = false ;
break ;
case EH:
( * T) -> bf = LH;
* taller = true ;
break ;
case RH:
( * T) -> bf = EH;
* taller = false ;
break ;
}
}
}
else
{
if ( ! insertAVL ( & ( * T) -> rchild, e, taller) )
return false ;
if ( * taller)
{
switch ( ( * T) -> bf)
{
case LH:
( * T) -> bf = EH;
* taller = false ;
break ;
case EH:
( * T) -> bf = RH;
* taller = true ;
break ;
case RH:
rightBalance ( T) ;
* taller = false ;
break ;
}
}
}
}
return true ;
}
5.2 红黑树
红黑树是一种自平衡的二叉查找树,它除了符合二叉查找树的基本特性外,还具有下列的附加特性。
(1)节点是红色或黑色。
(2)根节点是黑色。
(3)每个叶子节点都是黑色的空节点(NIL节点)。
(4)每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)。
(5)从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。 红黑树从根到叶子的最长路径不超过最短路径的两倍。
6 多路查找树
多路查找树结点的每个孩子数可以多于两个,且每个结点处可以存储多个元素,是一种平衡的查找树。 一般在大数据量且考虑外存的情况下使用。
6.1 2-3树
2-3树是一种多路查找树,其中每一个结点都有两个孩子(称为2结点)或者三个孩子(称为3结点),树中所有叶子都在同一层次上。 一个2结点包含一个元素和两个孩子(或者没有孩子)。左子树包含的元素小于该元素,右子树包含的元素大于该元素。 一个3结点包含一大一小两个元素和三个孩子(或者没有孩子)。左子树包含小于较小元素的元素,右子树包含大于较大元素的元素,中间子树包含介于两元素之间的元素。 同理,2-3-4树类似于2-3树的定义。
6.3 B树
B树是一种平衡的多路查找树,结点最大的孩子数目称为B树的阶。 2-3树和2-3-4树都是B树的特例。 一个m阶的B树具有如下属性:
(1)如果根节点不是叶节点,则其至少有两颗子树。
(2)所有叶子节点都位于同一层次。
(3)每一个非根的分支节点都有k-1个元素和k个孩子,其中[m/2]<=k<=m,每一个叶子结点n都有k-1个元素。
(4)所有分支节点包含下列信息数据(n,A0,K1,A1,K2,A2,…,Kn,An),其中K为关键字,Ki<Ki+1,Ai为指向子树根节点的指针。n为关键字的个数。 在查找的过程中通过使用B树这种数据结构,减少了必须访问结点和数据块的数量,B树这种数据结构就是为了内外存间的数据交换而准备的。
6.4 B+树
在B树中遍历时,我们可能需要往返于每个结点之间,如果结点大小与硬盘页面大小近似,这也意味着我们必须在硬盘页面之间进行多次访问。 B+树是应文件系统所需提出的一种B树的变形树,严格意义上讲它已不是树了。 B+树中出现在分支结点中的元素会被当作它们在该分支结点位置的中序后继者,而在叶子结点中再次列出,并且所有叶子结点都链接在一起。 B+树的结构特别适合带有范围的查找。 一颗m阶的B+树和m阶的B树的差异在于:
(1)所有的叶子结点包含全部关键字的信息,叶子结点本身依关键字的大小自小而大顺序链接。
(2)所有分支结点可以看成是索引,结点中只含有其子树中的最大(最小)关键字。
7 哈希查找(散列查找)
散列技术是在记录的存储位置和它的关键字之间建立一个确定的对应关系f,使得每个key对应一个存储位置f(key)。存储位置 = f(关键字) 这个f称为散列函数,又称为哈希函数。 采用散列技术将记录存储在一块连续的存储空间中,这块连续存储空间称为哈希表。 散列技术适合一对一的查找。 散列函数的构造方法:直接定址法、平方取中法、折叠法、除留余数法、随机数法 处理散列冲突的方法:开放定址法、随机探测法、再散列函数法、链地址法、公共溢出区法