数据结构学习笔记——查找

1.定义
查找(Searching)就是根据给定的某个值,在查找表中确定一个其关键字等于给定值的数据元素(或记录)。
查找概论
查找表(Search Table):由同一类型的数据元素(或记录)构成的集合叫查找表。
关键字(Key):关键字是数据元素中某个数据项的值,又称键值,用它可以标识一个数据元素。也可以标识一个记录的某个数据项(字段),我们称为关键码。
若此关键字可以唯一地表示一个记录,则称此关键字为主关键字(Primary Key)。
对于那些可以识别多个数据元素(或记录)的关键字,我们称为次关键字(Secondary Key)。
查找表按照操作方式来分有两大种:静态查找表和动态查找表
静态查找表(Static Search Table):只作查找操作的查找表。
动态查找表(Dynamic Search Table):在查找过程中同时插入查找表中不存在的数据元素,或者从查找表中删除已经存在的某个数据元素。
2.顺序表查找
顺序查找(Sequential Search)又叫线性查找,是最基本的查找技术,它的查找过程是:从表中第一个(或最后一个)记录开始,诸葛进行记录的关键字和给定值比较,若某个记录的关键字和给定值相等,则查找成功,找到所查的记录;如果找到最后一个(或第一个)记录,其关键字和给定值比较都不等时,则表中没有所查的记录,查找不成功。
下面是顺序查找的实现代码

//顺序查找,a为数组,n为要查找的数组长度,key为要查找的关键字
int Sequential_search(int *a, int n, int key){
	int i;
	for(i=1; i<=n; i++){
		if(a[i] == key)
			return i;
	}
	return 0;
}

下面是优化后的代码

//有哨兵顺序查找
int Sequential_Search2(int *a, int n, int key){
	int i;
	a[0] = key;              //设置a[0]为关键字值,我们称之为“哨兵”
	i = n;                   //循环从数组尾部开始
	while(a[i] != key){
		i--;
	}
	return 0;                //返回0则说明查找失败
}

这种在查找方向的尽头设置“哨兵”免去了在查找过程中每一次比较后都要判断查找位置是否越界的小技巧,看似与原先差别不大,但在总数据较多时,效率提高很大,是非常好的编码技巧。
3.有序表查找
(1)折半查找
折半查找(Binary Search)技术,又称二分查找。它的前提是线性表中的记录必须是关键码有序(通常从小到大有序),线性表必须采用顺序存储。折半查找的基本思想是:在有序表中,取中间记录作为比较对象,若给定值与中间记录的关键字相等,则查找成功;若给定值小于中间记录的关键字,则在中间记录的左半区继续查找;若给定值大于中间记录的关键字,则在中间记录的右半区继续查找。不断重复上述过程,直到查找成功,或所有查找区域无记录,查找失败为止。

//折半查找
int Binary_Search(int *a, int n, int key){
	int low, high, mid;
	low = 1;                        //定义最低下标为记录首位
	high = n;                       //定义最高下标为记录末位
	while(low <= high){
		mid = (low + high) / 2;     //折半
		if(key < a[mid])            //若查找值比中值小
			high = mid - 1;         //最高小标调整到中位下标小一位
		else if(key > a[mid])       //若查找值比中止大
			low = mid + 1;          //最低下标调整到中位下标大一位
		else
			return mid;             //若相等则说明mid即为查找到的位置
	}
	return 0;
}

(2)插值查找
插值查找(Interpolation Search)是根据要查找的关键字key与查找表中最大最小记录的关键字比较后的查找方法,其核心就在于插值的计算公式(key - a[low])/(a[high] - a[low])。
(3)斐波那契查找
斐波那契查找(Fibonacci Search)是利用了黄金分割原理来实现的。
下面是斐波那契查找的实现代码

int Fibonacci_search(int *a, int n, int key){
    int low, high, mid, i, k;
    low = 1;                     //定义最低小标为记录首位
    high = n;                    //定义最高小标为记录末位
    k = 0;
    while(n > F[k]-1)            //计算n位于斐波那契数列的位置
        k++;
    for(int i=0; i<F[k]-1; i++)  //将不满的数值补全
        a[i] = a[n];
    while(low <= high){
        mid = low + F[k-1] - 1;  //计算当前分割的小标
        if(key < a[mid]){        //若查找记录小于当前分隔记录
            high = mid - 1;      //最高小标调整到分隔下标mid-1
            k--;                 //斐波那契数列下标减一位
        }
        else if(key > a[mid]){   //若查找记录大于当前分隔记录
            low = mid + 1;       //最底下标调整到分隔下标mid+1处
            k = k - 2;
        }
        else{
            if(mid <= n)
                return mid;      //若相等则说明mid即为查找到的位置
            else
                return n;        //若mid>n说明是补全数值返回n
        }
    }
    return 0;
}

4.线性索引查找
数据结构的最终目的就是提高数据的处理速度,索引是为了加快查找速度而设计的一种数据结构。索引就是把一个关键字与它对应的记录相关联的过程,一个索引由若干个索引项构成,每个索引项至少应包含关键字和其对应的记录在存储器中的位置等信息。索引技术是组织大型数据库以及磁盘文件的一种重要技术。
索引按照结构可以分为线性索引、树形索引和多级索引。
所谓线性索引就是将索引项集合组织为线性结构,也称为索引表。
我们重点介绍三种线性索引:稠密索引、分块索引和倒排索引。
(1)稠密索引
稠密索引是指在线性索引中,将数据集中的每个记录对应一个索引项。
对于稠密索引这个索引表来说,索引项一定是按照关键码的有序的排列。
(2)分块索引
为了减少索引项的个数,我们可以对数据集进行分块,使其分块有序,然后再对每一块建立一个索引项,从而减少索引项的个数。
分块有序,是把数据集的记录分成了若干块,并且这些块需要满足两个条件:
块内无序,即每一块内的记录不要求有序。当然,你如果能够让块内有序对查找来说更理想,不过这就要付出大量时间和空间的代价,因此通常我们不要求块内有序。
块间有序,例如,要求第二块所有记录的关键字均要大于第一块中所有记录的关键字,第三块的所有记录的关键字均要大于第二块的所有记录关键字……因为只有块间有序,才有可能再查找时带来效率。
对于分块有序的数据集,将每块对应一个索引项,这种索引方法叫做分块索引
总的来说,分块索引在兼顾了对细分块不需要有序的情况下,大大增加了整体查找的速度,所以普遍被用于数据库表查找操作等技术的应用当中。
(3)倒排索引
索引项的通用结构是:
次关键码
记录号表

其中记录号表存储具有相同次关键字的所有记录的记录号(可以是指向记录的指针或者是该记录的主关键字)。这样的索引方法就是倒排索引(inverted index)。
这种索引表中的每一项都包括一个属性值和具有该属性值的各记录的地址。由于不是由记录来确定属性值,而是由属性值来确定记录的位置,因而称为倒排索引。
5.二叉排序树
二叉排序树(Binary Sort Tree),又称二叉查找树。它或者是一棵空树,或者是具有下列性质的二叉树。
若它的左子树不空,则左子树上所有结点的值均小于它的根结构的值;
若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
它的左、右子树也分别为二叉排序树。
从二叉排序树的定义也可以知道,它前提是二叉树,然后它采用了递归的定义方法,再者,它的结点间满足一定的次序关系,左子树结点一定比其双亲结点小,右子树结点一定比其双亲结点大。
构造一棵二叉排序树的目的,其实并不是为了排序,而是为了提高查找的插入和删除关键字的速度。不管怎么说,在一个有序数据集上查找,速度总是要快于无序的数据集的,而二叉排序树这种非线性的结构,也有利于插入和删除的实现。

typedef struct BiTNode{
    int data;
    struct BiTNode *lchild, *rchild;
}BiTNodem, *BiTree;

查找操作

//递归查找二叉排序树T中是否存在key
//指针f指向T的双亲,其初始调用值为NULL
//若查找成功,则指针p指向该数据元素结点,并返回TRUE
//否则指针p指向查找路径上访问的最后一个结点并返回FALSE
Status searchBST(BiTree T, int key, BiTree f, BiTree *p){
    if(!T){                     //查找不成功
        *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);   //在右子树继续查找
}

插入操作

//当二叉排序树T中不存在关键字等于key的数据元素时,
//插入key并返回TRUE,否则返回FALSE
Status InsearhBST(BiTree *T, int key){
    BiTree p, s;
    if( !searchBST(*T, key, NULL, &p) ){     //查找不成功
        s = (BiTree)malloc(sizeof(BiTNode));
        s->data = key;
        s->lchild = s->rchild = NULL;
        if( !p )
            *T = s;                          //插入s为新的根结点
        else if(key < p->data)
            p->lchild = s;                   //插入s为左孩子
        else
            p->rchild = s;                   //插入s为右孩子
        return TRUE;
    }
    else
        return FALSE;                        //树中已有关键字相同的结点,不再插入
}

删除操作

//若二叉排序树T中存在关键字等于key的数据元素时,则删除该数据元素结点,
//并返回TRUE,否则返回FALSE
Status DeleteBST(BiTree *T, int key){
    if( !*T )   //不存在关键字等于key的数据元素
        return FALSE;
    else{
        if( key == (*T)->data )           //找到关键字等于key的数据元素
            return Delete(T);
        else if( key < (*T)->data )
            return DeleteBST(&(*T)->lchild, key);
        else
            return DeleteBST(&(*T)->rchild, key);
    }
}
//从二叉排序树中删除结点p,并重接它的左或右子树
Status Delete(BiTree *p){
    BiTree q, s;
    if( (*p)->rchild == NULL ){            //右子树空则只需重接它的左子树
        q = *p;
        *p = (*p)->lchild;
        free(q);
    }
    else if( (*p)->lchild == NULL ){       //只需重接它的右子树
        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;              //s指向被删结点的直接前驱
        if( q != *p )
            q->rchild = s->rchild;         //重接q的右子树
        else
            q->lchild = s->lchild;         //重接q的左子树
        free(s);
    }
    return TRUE;
}

如果我们希望对一个集合按二叉排序树查找,最好是把它构建成一棵平衡的二叉排序树。
这样就引申出另一个问题,如何让二叉排序树平衡的问题。
6.平衡二叉树
平衡二叉树(Self-Balancing Binary Search Tree或Height-Balance Binary Search Tree),是一种二叉排序树,其中每一个结点的左子树和右子树的高度差至多等于1。
从二叉树的英文名字,我们可以体会到,它是一种高度平衡的二叉排序树
意思是说,要么它是一棵空树,要么它的左子树和右子树都是平衡二叉树,且左子树和右子树的深度之差的绝对值不超过1。我们将二叉树上结点的左子树深度减去右子树深度的值称为平衡因子BF(Balance Factor),那么平衡二叉树上所有结点的平衡因子只可能是-1、0和1。
距离插入结点最近的,且平衡因子的绝对值大于1的结点为根的子树,我们称为最小不平衡子树
平衡二叉树实现原理

//二叉树的二叉链表结点结构定义
typedef struct BiTNode{                 //结点结构
    int data;                           //结点数据
    int bf;                             //结点的平衡因子
    struct BiTNode *lchild, *rchild;    //左右孩子指针
}BiTNode, *BiTree;

右旋操作

//对以p为根的二叉排序树做右旋处理,
//处理之后p指向新的树根结点,即旋转处理之前的左子树的根节点
void R_Rotate(BiTree *P){
    BiTree L;
    L = (*P)->lchild;                //L指向P的左子树根节点
    (*P)->lchild = L->rchild;        //L的右子树挂接为P的左子树
    L->rchild = *P;
    *P = L;                          //P指向新的根节点
}

左旋操作

//对以P为根的二叉排序树作左旋处理,
//处理之后P指向新的树根结点,即旋转处理之前的右子树的根节点0
void L_Rotate(BiTree *P){
    BiTree R;
    R = (*P)->rchild;
    (*P)->rchild = R->lchild;
    R->lchild = (*P);
    (*P) = R;
}

左平衡旋转处理

#define LH +1
#define EH 0
#define RH -1
//对以指针T所指结点为根的二叉树作左平衡旋转处理
//本算法结束时,指针T指向新的根结点
void LeftBalance(BiTree *T){
    BiTree L, lr;
    L = (*T)->lchild;                //L指向T的左子树根结点
    switch(L->bf){                   //检查左子树的平衡度,并作相应平衡处理
        case LH:                     //新结点插入在T的左孩子的左子树上,要作单右旋处理
            (*T)->bf = L->bf = EH;
            R_Rotate(T);
            break;
        case RH:                     //新结点插入在T的左孩子的右子树上,要做双旋处理
            lr = L->rchild;          //Lr指向T的左孩子的右子树根
            switch(Lr->bf){          //修改T及其左孩子的平衡因子
                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;
            L_Rotate(&(*T)->rchild);  //对T的左子树作左旋平衡处理
            R_Rotate(T);              //对T作右旋平衡处理
    }
}

//若在平衡的二叉排序树T中不存在和e有相同关键字的结点,则插入一个
//数据元素为e的新结点并返回,否则返回0.若因插入而使二叉排序树
//失去平衡,则作平衡旋转处理,布尔变量taller反映T长高与否
Status InsertAVL(BiTree *T, int e, Status *taller){
    if(!*T){                  //插入新结点,树“长高”,置taller为TRUE
        *T = (BiTree)malloc(sizeof(BiTNode));
        (*T)->data = e;
        (*T)->lchild = (*T)->rchild = NULL;
        (*T)->bf = EH;
        *taller = TRUE;
    }
    else{
        if(e == (*T)->data){   //树中已存在和e有相同关键字的结点则不再插入
            *taller = FALSE;
            return FALSE;
        }
        if(e < (*T)->data){    //应继续在T的左子树中进行搜索
            if(!InsertAVL(&(*T)->lchild, e, taller))   //未插入
                return FALSE;
            if(*taller){       //已插入到T的左子树中且左子树“长高”
                switch((*T)->bf){        //检查T的平衡度
                    case LH:             //原本T的左子树比右子树高,需要作左平衡处理
                        LeftBalance(T);
                        *taller = FALSE;
                        break;
                    case EH:             //原本左右子树等高,现因左子树增高而树增高
                        (*T)->bf = LH;
                        *taller = TRUE;
                        break;
                    case RH:             //原本右子树比左子树高,现左右子树等高
                        (*T)->bf = EH;
                        *taller = FALSE;
                        break;
                }
            }
        }
        else{                   //应继续在T的右子树中进行搜索
            if(!InsertAVL(&(*T)->rchild, e, taller))   //未插入
                return FALSE;
            if(*taller){                  //已插入到T的左子树中且左子树“长高”
                switch((*T)->bf){         //检查T的平衡度
                    case LH:              //原本T的左子树比右子树高,现左、右子树等高
                        (*T)->bf = EH;
                        *taller = FALSE;
                        break;
                    case EH:              //原本左右子树等高,现因右子树增高而树增高
                        (*T)->bf = RH;
                        *taller = TRUE;
                        break;
                    case RH:              //原本右子树比左子树高,需要右平衡处理
                        RightBalance(T);
                        *taller = FALSE;
                        break;
                }
            }
        }
    }
    return TRUE;
}

6.多路查找树
多路查找树(muitl-way search tree),其每一个结点的孩子数可以多于两个,且每一个结点处可以存储多个元素。
(1)2-3树
2-3树是这样的一棵多路查找树:其中的每一个结点都具有两个孩子(我们称它为2结点)或三个孩子(我们称它为3结点)。
(2)2-3-4树
2-3-4树其实就是2-3树的概念扩展,包括了4结点的使用。一个4结点包含小中大三个元素和四个孩子(或没有孩子)。
(3)B树
B树(B-tree)是一种平衡的多路查找树,2-3树和2-3-4树都是B树的特例。结点最大的孩子数目称为B树的阶(order)。
(4)B+树

7.散列表(哈希表)
1.定义
散列技术是在记录的存储位置和它的关键字之间建立一个确定的对应关系f,使得每个关键字key对应一个存储位置f(key)。
这里我们把这种对应关系f称为散列函数,又称为哈希(Hash)函数。按这个思想,采用散列技术将记录存储在一块连续的存储空间中,这块连续存储空间称为散列表或哈希表(Hash table)。那么关键字对应的记录存储位置我们称为散列地址
散列技术既是一种存储方法,也是一种查找方法。
因此散列主要是面向查找的存储结构。
散列技术最适合的求解问题是查找与给定值相等的记录。
设计一个简单、均匀、存储利用率高的散列函数是散列技术中最关键的问题。
我们时常会碰到两个关键字key1 != key2,但是却有f(key1) = f(key2),这种现象我们称为冲突(collision),并把key1和key2称为这个散列函数的同义词(synonym)。
2.散列函数的构造方法
什么才算是好的散列函数?
1.计算简单
散列函数的计算时间不应该超过其他查找技术与关键字比较的时间。
2.散列地址分布均匀
我们刚才也提到冲突带来的问题,最好的办法就是尽量让散列地址均匀地分布在存储空间中,这样我们可以保证存储空间的有效利用,并减少为处理冲突而耗费的时间。
(1)直接定址法
我们可以取关键字的某个线性函数值为散列地址,即
f(key) = a * key + b(a、b为常数)
这样的散列函数优点就是简单、均匀,也不会产生冲突,但问题是这需要事先知道关键字的分布情况,适合查找表较小且连续的情况。
(2)数字分析法
这里我们提到了一个关键词——抽取。抽取方法是使用关键字的一部分来计算散列存储的方法,这在散列函数中是常常用到的手段。
数字分析法通常适合处理关键字位数比较大的情况,如果事先知道关键字的分布关键字的若干位分布较均匀,就可以考虑用这个方法。
(3)折叠法
折叠法是将关键字从左到右分割位数相等的几部分(注意最后一部分位数不够时可以短些),然后将这及部分叠加求和,并按散列表表长,取最后几位为散列地址
折叠法实现不需要知道关键字的分布,适合关键字位数较多的情况。
(4)除留余数法
此方法为最常用的构造散列函数方法。对于散列表长为m的散列函数公式为:
f(key) = key mod p (p<=m)
根据前辈们的经验,若散列表表长为m,通常p为小于或等于表长(最好接近m)的最小质数不包含小于20质因子的合数
(5)随机数法
f(key) = random(key)。
当关键字的长度不等时,采用这个方法构造散列函数时比较合适的。
总之,现实中,应该视不同的情况采用不同的散列函数。我们只能给出一些考虑的因素来提供参考:
1.计算散列地址所需的时间
2.关键字的长度。
3.散列表的大小。
4.关键字的分布情况
5.记录查找的频率。综合这个因素,才能决策选择哪种散列函数更合适。
3.处理散列冲突的方法
(1)开放定址法
开放定址法就是一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入。
f1(key) = (f(key) + di) MOD m(di = 1, 2, 3, ……, m-1)
我们把这种解决冲突的开放定址法称为线性探测法。
我们在解决冲突的时候,还会碰到本来都不是同义词却需要争夺一个地址的情况,我们称这种现象为堆积
增加平方运算的目的时为了不让关键字都聚集在某一块区域,我们称这种方法为二次探测法
fi(key) = (f(key) + di) MOD m (di = 12, -12……,q2, -q2, q <= m/2)
还有一种方法时,在冲突时,对于位移量di采用随机函数计算得到,我们称之为随机探测法
fi(key) = (f(key) + di) MOD m (di是一个随机数列)
(2)再散列函数法
fi(key) = RHi(key) (i=1, 2, ……,k)
这里RHi就是不同的散列函数,你可以把我们前面说的什么除留余数、折叠、平方取中全部用上。
(3)链地址法
将所有关键字为同义词的记录存储在一个单链表中,我们称这种表为同义词子表,在散列表中只存储所有同义词子表的头指针。
链地址法对于可能会造成很多冲突的散列函数来说,提供了绝不会出现找不到地址的保障。当然,这也就带来了查找是需要遍历单链表的性能损耗。
(4)公共溢出区法
我们为所有冲突的关键字建立了一个公共的溢出区来存放。
在查找时,对给定值通过散列函数计算出散列地址后,先与基本表的相应位置进行比对,如果相等,则查找成功;如果不想等,则到溢出区去进行顺序查找。
4.散列表查找实现
下面是散列表结构的实现代码

#define SUCCESS 1
#define UNSUCCESS 0
#define HASHISZE 12  //定义散列表长为数组的长度
#define NULLKEY -32768
typedef struct{
    int *elem;       //数据元素存储基址,动态分配数组
    int count;       //当前数据元素个数
}HashTable;
int m = 0;           //散列表表长,全局变量

初始化散列表

Status InitHashTable(HashTable *H){
    int i;
    m = HASHISZE;
    H->count = m;
    H->elem = (int *)malloc(m*sizeof(int));
    for(i=0; i<m; i++)
        H->elem[i] = NULLKEY;
    return OK;
}

定义散列函数

//散列函数
int Hash(int key){
    return key % m;  //除留余数法
}

插入操作

//插入关键字进散列表
void InsertHash(HashTable *H, int key){
    int addr = Hash(key);             //求散列地址
    while(H->elem[addr] != NULLKEY)   //如果不为空,则冲突
        addr = (addr+1) % m;          //开放定址法的线性探测
    H->elem[addr] = key;              //直到有空位后插入关键字
}

散列表查找关键字

Status SearchHash(HashTable H, int key, int *addr){
    *addr = Hash(key);              //求散列地址
    while(H.elem[*addr] != key){    //如果不为空,则冲突
        *addr = (*addr+1) % m;      //开放定址法的线性探测
        if(H.elem[*addr] == NULLKEY || *addr == Hash(key)){   //如果循环回到原点
            return UNSUCCESS;       //则说明关键字不存在
        }
    }
    return SUCCESS;
}

散列表查找性能分析
如果没有冲突,散列查找是我们本章介绍的所有查找中效率最高的,因为它的时间复杂度为O[1]。可惜,我说的只是“如果”,没有冲突的散列只是一种理想,在实际的应用中,冲突是不可避免的。
1.散列函数是否均匀
2.处理冲突的方法
3.散列表的装填因子
所谓的装填因子α = 填入表中的记录个数 / 散列表长度。
散列表的平均查找长度取决于装填因子,而不是取决于查找集合中的记录个数。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值