核心:从数据的第一个元素开始,依次比较,直到找到目标数据或查找失败。
1.从表中的第一个元素开始,依次与关键字比较。
2.若某个元素匹配关键字,则 查找成功。
3.若查找到最后一个元素还未匹配关键字,则 查找失败。
时间复杂度:顺序查找平均关键字匹配次数为表长的一半,其时间复杂度为O(n)。
顺序查找的评估:顺序查找的优点是对表无要求,插入数据可在O(1)内完成。缺点是时间复杂度较大,数据规模较大时,效率较低。
/* 顺序查找,Array为数组,n为要查找的数组元素个数,key为要查找的关键字*/
int seqSearch(int Array[], int key, int n)
{
for (int index = 0; index
也称折半查找,查找性能优异,但查找数据必须是有序序列。
核心:先确定待查目标所在的范围,然后逐步缩小范围直到查找成功或查找失败。
关键字key与表中某一元素Array[i]比较,有3中结果:
1.key==Array[i],查找成功。
2.key > Array[i],待查元素可能的范围是Array[i]之前。
3.key < Array[i],待查元素可能的范围是Array[i]之后。
二分查找基于上述的原理:每次将可能范围中间位置的数与key比较,相等则放回查找成功,不等则缩小范围。
时间复杂度:二分查找的时间复杂度为 log2(N)。
二分查找的评估:二分查找的效率较高,但要求序列有序。序列排序本身就是一种高代价操作,往有序序列内插入和删除数据都比较困难。因此,二分查找特别适合于很少改动,但需要经常查找的表。
/* 二分查找,Array为数组,n为要查找的数组元素个数,key为要查找的关键字*/
/*二分查找,非递归*/
int binarySearch(int Array[],int key,int n) {
int left=0;//记录范围左边界
int right = n - 1;//记录范围右边界
int mid;//范围是否不为空
while (left<=right)
{
mid = (right + left) / 2;
if (Array[mid]==key)
{
return mid;//查找成功返回
}
else if (Array[mid]>key)
{
right = mid - 1; //继续在右半边中查找
}
else
{
left = mid + 1;//继续在左半边中查找
}
}
return -1; // 当left>right时表示查找区间为空,查找失败
}
/* 二分查找,Array为数组,n为要查找的数组元素个数,key为要查找的关键字,left为范围左边界,right为范围右边界*/
/*二分查找,递归*/
int binarySearch1(int Array[],int left,int right,int key) {
int mid = (right + left) / 2;
if (Array[mid]==key)
return mid;//查找成功返回
else if (Array[mid]>key)
binarySearch1(Array,left, mid - 1,key);//递归调用,在左半边查询
else
binarySearch1(Array, mid + 1,right,key);//递归调用,在右半边查询
}
在介绍插值查找之前,首先考虑一个新问题,为什么上述算法一定要是折半,而不是折四分之一或者折更多呢?
打个比方,在英文字典里面查“apple”,你下意识翻开字典是翻前面的书页还是后面的书页呢?如果再让你查“zoo”,你又怎么查?很显然,这里你绝对不会是从中间开始查起,而是有一定目的的往前或往后翻。
同样的,比如要在取值范围1 ~ 10000 之间 100 个元素从小到大均匀分布的数组中查找5, 我们自然会考虑从数组下标较小的开始查找。
经过以上分析,折半查找这种查找方式,不是自适应的(也就是说是傻瓜式的)。二分查找中查找点计算如下:
mid=(low+high)/2, 即mid=low+1/2*(high-low);
通过类比,我们可以将查找的点改进为如下:
mid=low+(key-a[low])/(a[high]-a[low])*(high-low),
也就是将上述的比例参数1/2改进为自适应的,根据关键字在整个有序表中所处的位置,让mid值的变化更靠近关键字key,这样也就间接地减少了比较次数。
基本思想:基于二分查找算法,将查找点的选择改进为自适应选择,可以提高查找效率。当然,差值查找也属于有序查找。
注:对于表长较大,而关键字分布又比较均匀的查找表来说,插值查找算法的平均性能比折半查找要好的多。反之,数组中如果分布非常不均匀,那么插值查找未必是很合适的选择。
复杂度分析:查找成功或者失败的时间复杂度均为O(log2(log2n))。
//插值查找
int InsertionSearch(int Array[],int left,int right,int key) {
int mid = left + (key - Array[left]) / (Array[right] - Array[left])*(right - left);//插值公式
if (Array[mid] == key)
return mid;
if (Array[mid]>key)
return InsertionSearch(Array, key, left, mid - 1);
if (Array[mid]
相对于二分查找和差值查找,斐波那契查找的实现略显复杂。但是在明白它的主体思想之后,掌握起来也并不太难。
既然叫斐波那契查找,首先得弄明白什么是斐波那契数列。相信大家对这个著名的数列也并不陌生,无论是C语言的循环、递归,还是高数的数列,斐波那契数列都是一个重要的存在。而此处主要是用到了它的一条性质:前一个数除以相邻的后一个数,比值无限接近黄金分割。
就笔者而言,这种查找的精髓在于采用最接近查找长度的斐波那契数值来确定拆分点,初次接触的童鞋,请在读完下文后,自觉回过头来仔细体会这句话。举个例子来讲,现有长度为9的数组,要对它进行拆分,对应的斐波那契数列(长度先随便取,只要最大数大于9即可){1,1,2,3,5,8,13,21,34},不难发现,大于9且最接近9的斐波那契数值是f[6]=13,为了满足所谓的黄金分割,所以它的第一个拆分点应该就是f[6]的前一个值f[5]=8,即待查找数组array的第8个数,对应到下标就是array[7],依次类推。
推演到一般情况,假设有待查找数组array[n]和斐波那契数组F[k],并且n满足n>=F[k]-1&&n < F[k+1]-1,则它的第一个拆分点middle=F[k]-1。
这里得注意,如果n刚好等于F[k]-1,待查找数组刚好拆成F[k-1]和F[k-2]两部分,那万事大吉你好我好;然而大多数情况并不能尽人意,n会小于F[k]-1,这时候可以拆成完整F[k-1]和残疾的F[k-2]两部分,那怎么办呢?
聪明的前辈们早已想好了解决办法,对了,就是补齐,用最大的数来填充F[k-2]的残缺部分,如果查找的位置落到补齐的部分,那就可以确定要找的那个数就是最后一个最大的了。
(只是为了确定一个分割点,其余的思想和二分查找一样)
斐波那契查找与折半查找很相似,他是根据斐波那契序列的特点对有序表进行分割的。他要求开始表中记录的个数为某个斐波那契数小1,即n=F(k)-1;
开始将k值与第F(k-1)位置的记录进行比较(及mid=low+F(k-1)-1),比较结果也分为三种
1)相等,mid位置的元素即为所求
2)> ,low=mid+1,k-=2;说明:low=mid+1说明待查找的元素在[mid+1,hign]范围内,k-=2 说明范围[mid+1,high]内的元素个数为n-(F(k-1))= Fk-1-F(k-1)=Fk-F(k-1)-1=F(k-2)-1个,所以可以递归的应用斐波那契查找
3)< ,high=mid-1,k-=1;说明:high=mid-1说明待查找的元素在[low,mid-1]范围内,k-=1 说明范围[low,mid-1]内的元素个数为F(k-1)-1个,所以可以递归的应用斐波那契查找
大部分说明都忽略了一个条件的说明:n=F(k)-1, 表中记录的个数为某个斐波那契数小1。这是为什么呢?
我想了很久,终于发现,原因其实很简单:是为了格式上的统一,以方便递归或者循环程序的编写。表中的数据是F(k)-1个,使用mid值进行分割又用掉一个,那么剩下F(k)-2个。正好分给两个子序列,每个子序列的个数分别是F(k-1)-1与F(k-2)-1个,格式上与之前是统一的。不然的话,每个子序列的元素个数有可能是F(k-1),F(k-1)-1,F(k-2),F(k-2)-1个,写程序会非常麻烦。
斐波那契查找的核心是:
1)当key=a[mid]时,查找成功;
2)当key时,新的查找范围是第low个到第mid-1个,此时范围个数为F[k-1] - 1个,即数组左边的长度,所以要在[low, F[k - 1] - 1]范围内查找;
3)当key>a[mid]时,新的查找范围是第mid+1个到第high个,此时范围个数为F[k-2] - 1个,即数组右边的长度,所以要在[F[k - 2] - 1]范围内查找。
条件:(1)数据必须采用顺序存储结构;(2)数据必须有序。
原理:(1)最接近查找长度的斐波那契值来确定拆分点;(2)黄金分割。
时间复杂度:与拆半查找一样,也是logn。
const int max_size = 20;//斐波那契数组的长度
/*构造一个斐波那契数组*/
void Fibonacci(int * F)
{
F[0] = 0;
F[1] = 1;
for (int i = 2; iF[k] - 1)//计算n位于斐波那契数列的位置
++k;
int * temp;//将数组a扩展到F[k]-1的长度
temp = new int[F[k] - 1];
memcpy(temp, a, n * sizeof(int));
//temp是数组a的复制
//temp中有F[k] - 1个数,但是a中只有n个,所以剩余的数用a中的最大值即a[n-1]填充。
for (int i = n; itemp[mid])
{
low = mid + 1;
k -= 2;
}
else
{
if (mid=n则说明是扩展的数值,返回n-1
}
}
delete[] temp;
return -1;
}
//递归实现
int FibonacciSearch1(int *a,int *F, int key, int low, int hight,int k)
{
int middle = low + F[k - 1] - 1;
if (key < a[middle]) {
return FibonacciSearch1(a, F, key, low, middle - 1, k - 1);
}
else if (key > a[middle]) {
return FibonacciSearch1(a, F, key, middle + 1, hight, k - 2);
}
else {
if (middle <= hight) {
return middle;
}
else {
return hight;
}
}
}
二叉查找树定义:又称为是二叉排序树(Binary Sort Tree)或二叉搜索树。二叉排序树或者是一棵空树,或者是具有下列性质的二叉树:
1) 若左子树不空,则左子树上所有结点的值均小于它的根结点的值;
2) 若右子树不空,则右子树上所有结点的值均大于或等于它的根结点的值;
3) 左、右子树也分别为二叉排序树;
4) 没有键值相等的节点。
二叉查找树的性质:对二叉查找树进行中序遍历,即可得到有序的数列。
二叉查找树的时间复杂度:它和二分查找一样,插入和查找的时间复杂度均为O(logn),但是在最坏的情况下仍然会有O(n)的时间复杂度。原因在于插入和删除元素的时候,树没有保持平衡(比如,我们查找上图(b)中的“93”,我们需要进行n次查找操作)。我们追求的是在最坏的情况下仍然有较好的时间复杂度,这就是平衡查找树设计的初衷。
二叉查找树的高度决定了二叉查找树的查找效率。
二叉查找树的插入过程如下:
1) 若当前的二叉查找树为空,则插入的元素为根节点;
2) 若插入的元素值小于根节点值,则将元素插入到左子树中;
3) 若插入的元素值不小于根节点值,则将元素插入到右子树中。
二叉查找树的删除,分三种情况进行处理:
1) p为叶子节点,直接删除该节点,再修改其父节点的指针(注意分是根节点和不是根节点)
2) p为单支节点(即只有左子树或右子树)。让p的子树与p的父亲节点相连,删除p即可(注意分是根节点和不是根节点)
3) p的左子树和右子树均不空。找到p的后继y,因为y一定没有左子树,所以可以删除y,并让y的父亲节点成为y的右子树的父亲节点,并用y的值代替p的值;或者方法二是找到p的前驱x,x一定没有右子树,所以可以删除x,并让x的父亲节点成为y的左子树的父亲节点。如图c。
/*********************************
二叉排序树的相关操作实现
Author:兰亭风雨 Date:2014-02-23
Email:[email protected]
**********************************/
#include
#include
typedef struct Node
{
int data;
struct Node *lchild;
struct Node *rchild;
}NODE, *BSTree;
/*
在指针pTree所指的二叉排序树中递归查找关键字为key的元素,
若查找成功,则返回指向该元素节点的指针,否则返回NULL
*/
BSTree search(BSTree pTree, int key)
{
if (!pTree || pTree->data == key) //查找到时返回的pTree为该元素节点,没查找到时为NULL
return pTree;
else if (key < pTree->data) //如果key小于当前节点的值,则在其左子树中递归查找
return search(pTree->lchild, key);
else //如果key大于当前节点的值,则在其右子树中递归查找
return search(pTree->rchild, key);
}
/*
在指针pTree所指的二叉排序树中递归查找关键字为key的元素,
若查找成功,则返回ture,并查找到的数据对应的节点指针保存在p中,
否则返回false,并将查找路径上访问的最后一个节点指针保存在p中。
这里的参数parent指向每次递归遍历的子树的根节点的父节点,即始终是参数pTree的父节点,
它的初始值为NULL,其目的是跟踪查找路径上访问的当前节点的父节点(即上一个访问节点)
该函数用来被后面的插入函数调用。
*/
bool search_BSTree(BSTree pTree, int key, BSTree parent, BSTree &p)
{
if (!pTree) //如果pTree为NULL,则查找不成功
{ //这里包含了树空,即pTree为NULL的情况
p = parent;
return false;
}
else //否则,继续查找
{
if (key == pTree->data) //如果相等,则查找成功
{
p = pTree;
return true;
}
else if (key < pTree->data) //在左子树中递归查找
return search_BSTree(pTree->lchild, key, pTree, p);
else //在右子树中递归查找
return search_BSTree(pTree->rchild, key, pTree, p);
}
}
/*
当在pTree所指向的二叉排序树中查找不到关键字为key的数据元素时,
将其插入该二叉排序树,并返回ture,否则返回false。
树空时插入会改变根节点的值,因此要传入引用。
*/
bool insert(BSTree &pTree, int key)
{
BSTree p;
if (!search_BSTree(pTree, key, NULL, p)) //如果查找失败,则执行插入操作
{
//为新节点分配空间,并对各域赋值
BSTree pNew = (BSTree)malloc(sizeof(NODE));
pNew->data = key;
pNew->lchild = pNew->rchild = NULL;
if (!p) //如果树空,则直接置pNew为根节点
pTree = pNew;
else if (key < p->data) //作为左孩子插入p的左边
p->lchild = pNew; //作为右孩子插入p的右边
else
p->rchild = pNew;
}
else
return false;
}
/*
采用第一种算法从二叉排序树中删除指针p所指向的节点,
并在保持二叉排序树有序的情况下,将其左右子树重接到该二叉排序树中.
该函数主要用来被后面的删除函数调用
*/
void delete_Node1(BSTree &p)
{
BSTree q, s;
if (!p->lchild)
{ //如果左子树为空,则只需重接其右子树
//这里包含了左右子树均为空的情况
q = p;
p = p->rchild;
free(q);
}
else if (!p->rchild)
{ //如果右子树为空,则只需重接其左子树
q = p;
p = p->lchild;
free(q);
}
else
{ //如果左右子树都不为空,我们采取第一种方法来重接左右子树,
//我们这里采取修改左子树的方法,也可以修改右子树,方法类似
s = p->lchild; //取待删节点的左节点
//一直向右,最终s为待删节点的前驱节点
//如果将各节点元素按从小到大顺序排列成一个序列,
//则某节点的前驱节点即为序列中该节点的前面一个节点
while (s->rchild)
s = s->rchild;
s->rchild = p->rchild; //将p的右子树接为s的右子树
q = p;
p = p->lchild; //将p的左子树直接接到其父节点的左子树上
free(q);
}
}
/*
采用第二种算法从二叉排序树中删除指针p所指向的节点,
并在保持二叉排序树有序的情况下,将其左右子树重接到该二叉排序树中.
该函数主要用来被后面的删除函数调用
*/
void delete_Node2(BSTree &p)
{
BSTree q, s;
if (!p->lchild)
{ //如果左子树为空,则只需重接其右子树
//这里包含了左右子树均为空的情况
q = p;
p = p->rchild;
free(q);
}
else if (!p->rchild)
{ //如果右子树为空,则只需重接其左子树
q = p;
p = p->lchild;
free(q);
}
else
{ //如果左右子树都不为空,我们采取第二种方法来重接左右子树,
//我们这里采取修改左子树的方法,也可以修改右子树,方法类似
q = p;
s = p->lchild; //取待删节点的左节点
while (s->rchild)
{ //一直向右,最终s为待删节点的前驱节点。
//如果将各节点元素按从小到大顺序排列成一个序列,
//则某节点的前驱节点即为序列中该节点的前面一个节点
q = s;
s = s->rchild;
}
//用s来替换待删节点p
p->data = s->data;
//根据情况,将s的左子树重接到q上
if (p != q)
q->rchild = s->lchild;
else
q->lchild = s->lchild;
free(s);
}
}
/*
若pTree所指向的二叉排序树中查找到关键字为key的数据元素,
则删除该元素对应的节点,并返回true,否则返回false
如果要删除的恰好是根节点,则会改变根节点的值,因此要传入引用
*/
bool delete_BSTree(BSTree &pTree, int key)
{
//不存在关键字为key的节点
if (!pTree)
return false;
else
{
if (key == pTree->data) //查找到关键字为key的节点
{
delete_Node1(pTree);
// delete_Node2(pTree);
return true;
}
else if (key < pTree->data) //继续查找左子树
return delete_BSTree(pTree->lchild, key);
else //继续查找右子树
return delete_BSTree(pTree->rchild, key);
}
}
/*
根据所给的长为len的arr数组,按数组中元素的顺序构建一棵二叉排序树
*/
BSTree create_BSTree(int *arr, int len)
{
BSTree pTree = NULL;
int i;
//按顺序逐个节点插入到二叉排序树中
for (i = 0; ilchild)
in_traverse(pTree->lchild);
printf("%d ", pTree->data);
if (pTree->rchild)
in_traverse(pTree->rchild);
}
}
int main()
{
int i;
int num;
printf("请输入节点个数:");
scanf("%d", &num);
//输入num个整数
int *arr = (int *)malloc(num * sizeof(int));
printf("请依次输入这%d个整数(必须互不相等):", num);
for (i = 0; i
分块查找又称索引顺序查找,它是顺序查找的一种改进方法。
算法思想:将n个数据元素"按块有序"划分为m块(m ≤ n)。每一块中的结点不必有序,但块与块之间必须"按块有序";即第1块中任一元素的关键字都必须小于第2块中任一元素的关键字;而第2块中任一元素又都必须小于第3块中的任一元素,……
算法流程:
step1 先选取各块中的最大关键字构成一个索引表;
step2 查找分两个部分:先对索引表进行二分查找或顺序查找,以确定待查记录在哪一块中;然后,在已确定的块中用顺序法进行查找。
typedef int KeyType;
typedef char InfoType[10];
typedef struct
{
KeyType key; //KeyType为关键字的数据类型
InfoType data; //其他数据
} NodeType;
typedef NodeType SeqList[MAXL]; //顺序表类型
typedef struct
{
KeyType key; //KeyType为关键字的类型
int link; //指向对应块的起始下标
} IdxType;
typedef IdxType IDX[MAXI]; //索引表类型
//I表示索引表如:IDX I = { { 14,0 },{ 34,5 },{ 66,10 },{ 85,15 },{ 100,20 } };
//m表示分块的数量如:m=5;
//R表示需要查找的数据表 如:KeyType a[] = { 8,14,6,9,10, 22,34,18,19,31, 40,38,54,66,46, 71,78,68,80,85, 100,94,88,96,87 };
//n表示a[]中的个数 n=25;
//K表示需要查找的数值 K=85;
int IdxSearch(IDX I, int m, SeqList R, int n, KeyType k)
{ //二分查找法查找索引表
int low = 0, high = m - 1, mid, i;
int b = n / m; //b为每块的记录个数
while (low <= high) //在索引表中进行二分查找,找到的位置存放在low中
{
mid = (low + high) / 2;
if (I[mid].key >= k)
high = mid - 1;
else
low = mid + 1;
}
//应在索引表的high+1块中,再在线性表中进行顺序查找
i = I[high + 1].link;
while (i <= I[high + 1].link + b - 1 && R[i].key != k) //在顺序表中固定的块中查找
i++;
if (i <= I[high + 1].link + b - 1)
return i + 1; //返回查找成功后该数值在顺序表中的序号
else
return 0;
}