阅读目录
说是七种,其实二分查找、插值查找以及斐波那契查找都可以归为一类——插值查找。插值查找和斐波那契查找是在二分查找的基础上的优化查找算法。
查找定义:根据给定的某个值,在查找表中确定一个其关键字等于给定值的数据元素(或记录)。
1. 顺序查找
当查找不成功时,需要n+1次比较,时间复杂度为O(n);
int Sequential_Search2(int *a, int n, int key)
{
int i;
a[0] = key;//设置a[0]为关键字值,我们称之为哨兵
i = n;//循环从数组尾部开始
while (a[i] != key)//设置哨兵,可以不用检查i是否越界
{
i--;
}
return i;//查找失败
}
2. 二分查找
说明:元素必须是有序的,如果是无序的则要先进行排序操作。
基本思想:也称为是折半查找,属于有序查找算法。用给定值k先与中间结点的关键字比较,中间结点把线形表分成两个子表,若相等则查找成功;若不相等,再根据k与该中间结点关键字的比较结果确定下一步查找哪个子表,这样递归进行,直到查找到或查找结束发现表中没有这样的结点。
复杂度分析:最坏情况下,关键词比较次数为log2(n+1),且期望时间复杂度为O(log2n);
注:折半查找的前提条件是需要有序表顺序存储,对于静态查找表,一次排序后不再变化,折半查找能得到不错的效率。但对于需要频繁执行插入或删除操作的数据集来说,维护有序的排序会带来不小的工作量,那就不建议使用。——《大话数据结构》
C++代码
/*二分查找
非递归算法*/
int Binary_Search(int *a,int low,int high,int key)
{
int mid;
while(low<=high)//必须写成low<=high,不能写low!=high ,因为当key为第一个或者最后一个数据时,high==low
{
mid=(low+high)/2;//折半
if(key<a[mid])
high=mid-1;
else if(key>a[mid])
low=mid+1;
else return mid;
}
return -1;
}
[cpp] view plain copy
/*二分查找
递归算法*/
int Binary_Search(int *a,int low,int high,int key)
{
int mid;
if(low>high)
return -1;//没找到,递归结束
else{
mid=(low+high)/2;
if(a[mid]==key)
return mid;//找到了,递归结束
else if(a[mid]<key)
return Binary_Search(a,mid+1,high,key);//右递归
else
return Binary_Search(a,low,mid-1,key);//左递归
}
}
3. 插值查找
4. 斐波那契查找
斐波那契数列,又称黄金分割数列,指的是这样一个数列:1、1、2、3、5、8、13、21、····,在数学上,斐波那契被递归方法如下定义:F(1)=1,F(2)=1,F(n)=f(n-1)+F(n-2) (n>=2)。该数列越往后相邻的两个数的比值越趋向于黄金比例值(0.618)。
基本算法:
1、斐波那契查找就是在二分查找的基础上根据斐波那契数列进行分割的。
2、在斐波那契数列找一个等于略大于查找表中元素个数的数F[n],将原查找表扩展为长度为F[n](如果要补充元素,则补充重复最后一个元素,直 到满足F[n]个元素),完成后进行斐波那契分割,即F[n]个元素分割为前半部分F[n-1]个元素,后半部分F[n-2]个元素,找出要查找的元素在那一 部分并递归,直到找到。
复杂度分析:最坏情况下,时间复杂度为O(log2n),且其期望复杂度也为O(log2n)。
与折半查找相比,斐波那契查找的优点是它只涉及加法和减法运算,而不用除法,而除法比加减法要占用更多的时间,因此,斐波那契查找的运行时间理论上比折半查找小,但是还是得视具体情况而定。
举例分析该算法:
对于斐波那契数列:1、1、2、3、5、8、13、21、34、55、89……(也可以从0开始),前后两个数字的比值随着数列的增加,越来越接近黄金比值0.618。比如这里的89,把它想象成整个有序表的元素个数,而89是由前面的两个斐波那契数34和55相加之后的和,也就是说把元素个数为89的有序表分成由前55个数据元素组成的前半段和由后34个数据元素组成的后半段,那么前半段元素个数和整个有序表长度的比值就接近黄金比值0.618,假如要查找的元素在前半段,那么继续按照斐波那契数列来看,55 = 34 + 21,所以继续把前半段分成前34个数据元素的前半段和后21个元素的后半段,继续查找,如此反复,直到查找成功或失败,这样就把斐波那契数列应用到查找算法中了。
/*斐波那契数列查找*/
int Fibonnacci_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 (i = n; i < F[k] - 1; i++)//将不满的数值补齐,其中F数组为斐波那契数列
a[i] = a[n];
while (low < =high)
{
mid = low + F[k - 1] - 1;//计算当前分割的下标
if (key < a[mid])
{
high = mid - 1;
// (全部元素) = (前半部分)+(后半部分)
// f[k] = f[k-1] + f[k-2]
// 因为前半部分有f[k-1]个元素,所以 k = k-1
k = k - 1;//斐波那契数列下标减一位
}
else if (key > a[mid])
{
low = mid + 1;
// 因为后半部分有f[k-2]个元素,所以 k = k-2
k = k - 2;//斐波那契下标减两位
}
else {
if (mid <= n)
return mid;//若相等,说明mid即为查找到的位置
else return n;//若mid>n说明是补全数值,返回n
}
}
return 0;
}
5. 树表查找
5.1 最简单的树表查找算法——二叉树查找算法。
基本思想:二叉查找树是先对待查找的数据进行生成树,确保树的左分支的值小于右分支的值,然后在就行和每个节点的父节点比较大小,查找最适合的范围。 这个算法的查找效率很高,但是如果使用这种查找方法要首先创建树。
二叉树:
每个结点最多有两个子树的树结构且左子树和右子树是有顺序的,不能颠倒,即使树中某结点只有一颗子树,也要区分它是左子树还是右子树
1、已知前序遍历序列和中序遍历序列,可以唯一确定一颗二叉树
2、已知后序遍历序列和中序遍历序列,可以唯一确定一颗二叉树。
但是已知前序遍历和后序遍历,是不能确定一颗二叉树的。因为无法确定左子树和右子树。比如前序遍历是ABC,后序遍历是CBA.
则有以下四种情形:
前序遍历(根,左,右)、中序遍历(左,根,右)、后序遍历(左,右,根)都是针对根结点而言的。
二叉查找树(排序树、搜索树):
1、性质:
1、可以是空树
2、若左子树不空,则左子树上所有结点的值均严格小于它的根结点的值
若右子树不空,则右子树上所有结点的值均严格大于它的根结点的值
3、它的左右子树也是二叉排序树。
4、对它进行中序排列时,可以得到一个从小到大排列的有序序列
5、没有键值相等的点
二叉搜索树是以链接的方式存储,当需要插入或者删除元素时,只需修改链接指针即可。
2、数据结构
typedef struct node
{
KeyType key ; /*关键字的值*/
struct node *lchild,*rchild;/*左右指针*/
}BSTNode, *BSTree;
3、二叉排序树的创建
可以将树结点逐个插入到二叉排序树(一开始可以是一颗空树)中,只要保证插入后,依然满足二叉排序树的特点,就可以创建一个二叉排序树。
设树结点的关键字值为key
算法思想:
1)若二叉排序树是空树,则将key结点成为二叉排序树的根结点。
2)若二叉排序树非空树,则将key与二叉排序树的根进行比较:
a.如果key的值等于根结点的值,则停止插入。
b.如果key的值小于根结点的值,则将key所在结点插入左子树。
c.如果key的值大于根结点的值,则将key所在结点插入右子树。
算法实现:
void InsertBST(BSTree *bst, KeyType key)
/*若在二叉排序树中不存在关键字等于key的元素,插入该元素*/
{
BSTree s;
if (*bst == NULL)/*递归结束条件*/
{
s=(BSTree)malloc(sizeof(BSTNode));/*申请新的结点s*/
s-> key=key;
s->lchild=NULL;
s->rchild=NULL;
*bst=s;
}
else {
if (key < (*bst)->key)
InsertBST(&((*bst)->lchild), key);/*将s插入左子树*/
else
if (key > (*bst)->key)
InsertBST(&((*bst)->rchild), key); /*将s插入右子树*/
}
}
4、二叉查找树的查找
因为二叉排序树是可以看作是一个有序表,所以其查找过程和折半查找类似。
算法思想:
首先将待查关键字key与根节点关键字t进行比较:
a.如果key = t, 则返回根节点指针。
b.如果key < t,则进一步查找左子树。
c.如果key > t,则进一步查找右子树。
1)递归算法实现:
/*在根指针bst所指二叉排序树中,递归查找某关键字等于key的元素,若查找成功,返回指向该元素结点指针,否则返回空*/
BSTree SearchBST(BSTree bst, KeyType key) {
if (!bst)
return NULL;
else
if (bst->key == key)
return bst;/ *查找成功* /
else {
if (bst->key > key)
return SearchBST(bst->lchild, key); /*在左子树继续查找*/
else
return SearchBST(bst->rchild, key); /*在右子树继续查找*/
}
}
2)非递归实现
BSTree SearchBST(BSTree bst, KeyType key)
/*在根指针bst所指二叉排序树bst上,查找关键字等于key的结点,若查找成功,返回指向该元素结点指针,否则返回空指针*/
{
BSTree q;
q=bst;
while(q)
{
if (q->key == key)
return q; /*查找成功*/
if (q->key > key)
q=q->lchild; /*在左子树中查找*/
else
q=q->rchild; /*在右子树中查找*/
}
return NULL; /*查找失败*/
}
4、二叉排序树的插入:
/*递归写法*/
BSTNode * insertNode(BSTNode * root, BSTNode * node) {
BSTNode*pre=root;
if(pre==NULL)
{ pre=node;
return pre;}
if(pre->val<node->val)
pre->rchild=insertNode(pre->rchild,node);
else
pre->lchild=insertNode(pre->lchild,node);
return root;
}
/*非递归写法*/
BSTNode * InsertNode(BSTNode * root, BSTNode * node) {
if(root==NULL)
{
root=node;
return root;
}
TreeNode*cur=root;
while(cur!=NULL)
{
if(cur->val<node->val)
{ if(cur->rchild==NULL)
{ cur->rchild=node;
return root; }//必须写return root不然将陷入无限循环中
cur=cur->rchild;
}
else
{ if(cur->lchild==NULL)
{ cur->lchild=node;
return root; }
cur=cur->lchild;
}
}
return root;
}
5、二叉查找树的删除:
思路:
1、先找到待删除点p
2 如果p不存在,则返回原二叉树
3、p存在
p无左子树:1、p的父节点f为NULL,则p为根结点,直接返回p的右子树
2、p不是根结点
p是f的左子树,将p的右子树链到f的左链上
p是f的右子树,将p的右子树链到f的右链上
p有左子树:1、在p的左子树中查找最右结点
1、如果p的左子树的右子树不存在(p==q)
2、如果p的左子树的右子树存在(p!=q)
BSTNode * DelBST(BSTree t, KeyType k) /*在二叉排序树t中删去关键字为k的结点*/
{
BSTNode *p, *f,*s ,*q;
p=t;
f=NULL;
while(p) /*查找关键字为k的待删结点p*/
{
if(p->key==k ) break; /*找到则跳出循环*/
f=p; /*f指向p结点的双亲结点*/
if(p->key>k)
p=p->lchild;
else
p=p->rchild;
}
if(p==NULL) return t; /*若找不到,返回原来的二叉排序树*/
if(p->lchild==NULL) /*p无左子树*/
{
if(f==NULL)
t=p->rchild; /*p是原二叉排序树的根*/
else
if(f->lchild==p) /*p是f的左孩子*/
f->lchild=p->rchild ; /*将p的右子树链到f的左链上*/
else /*p是f的右孩子*/
f->rchild=p->rchild ; /*将p的右子树链到f的右链上*/
free(p); /*释放被删除的结点p*/
}
else /*p有左子树*/
{
q=p;
s=p->lchild;
while(s->rchild) /*在p的左子树中查找最右结点*/
{
q=s;
s=s->rchild;
}
if(q==p)
q->lchild=s->lchild ; /*重接q的左子树*/
else
q->rchild=s->lchild;/*重接q的右子树*/
p->key=s->key; /*将s的值赋给p*/
free(s);
}
return t;
} /*DelBST*/
6、完整代码描述:
#include <stdio.h>
#include <stdlib.h>
#define ENDKEY 0
typedef int KeyType;
typedef struct node
{
KeyType key ; /*关键字的值*/
struct node *lchild,*rchild;/*左右指针*/
}BSTNode, *BSTree;
void InsertBST(BSTree *bst, KeyType key)
/*若在二叉排序树中不存在关键字等于key的元素,插入该元素*/
{
BSTree s;
if (*bst == NULL)/*递归结束条件*/
{
s=(BSTree)malloc(sizeof(BSTNode));/*申请新的结点s*/
s-> key=key;
s->lchild=NULL;
s->rchild=NULL;
*bst=s;
}
else
if (key < (*bst)->key)
InsertBST(&((*bst)->lchild), key);/*将s插入左子树*/
else
if (key > (*bst)->key)
InsertBST(&((*bst)->rchild), key); /*将s插入右子树*/
}
void CreateBST(BSTree *bst)
/*从键盘输入元素的值,创建相应的二叉排序树*/
{
KeyType key;
*bst=NULL;
scanf("%d", &key);
while (key!=ENDKEY) /*ENDKEY为自定义常量*/
{
InsertBST(bst, key);
scanf("%d", &key);
}
}
void PreOrder(BSTree root)
/*先序遍历二叉树, root为指向二叉树根结点的指针*/
{
if (root!=NULL)
{
printf("%d ",root->key); /*输出结点*/
PreOrder(root->lchild); /*先序遍历左子树*/
PreOrder(root->rchild); /*先序遍历右子树*/
}
}
/*
BSTree SearchBST(BSTree bst, KeyType key)
/ *在根指针bst所指二叉排序树中,递归查找某关键字等于key的元素,若查找成功,返回指向该元素结点指针,否则返回空指针* /
{
if (!bst)
return NULL;
else
if (bst->key == key)
return bst;/ *查找成功* /
else
if (bst->key > key)
return SearchBST(bst->lchild, key);/ *在左子树继续查找* /
else
return SearchBST(bst->rchild, key);/ *在右子树继续查找* /
}*/
BSTree SearchBST(BSTree bst, KeyType key)
/*在根指针bst所指二叉排序树bst上,查找关键字等于key的结点,若查找成功,返回指向该元素结点指针,否则返回空指针*/
{
BSTree q;
q=bst;
while(q)
{
if (q->key == key)
return q; /*查找成功*/
if (q->key > key)
q=q->lchild; /*在左子树中查找*/
else
q=q->rchild; /*在右子树中查找*/
}
return NULL; /*查找失败*/
}/*SearchBST*/
BSTNode * DelBST(BSTree t, KeyType k) /*在二叉排序树t中删去关键字为k的结点*/
{
BSTNode *p, *f,*s ,*q;
p=t;
f=NULL;
while(p) /*查找关键字为k的待删结点p*/
{
if(p->key==k ) break; /*找到则跳出循环*/
f=p; /*f指向p结点的双亲结点*/
if(p->key>k)
p=p->lchild;
else
p=p->rchild;
}
if(p==NULL) return t; /*若找不到,返回原来的二叉排序树*/
if(p->lchild==NULL) /*p无左子树*/
{
if(f==NULL)
t=p->rchild; /*p是原二叉排序树的根*/
else
if(f->lchild==p) /*p是f的左孩子*/
f->lchild=p->rchild ; /*将p的右子树链到f的左链上*/
else /*p是f的右孩子*/
f->rchild=p->rchild ; /*将p的右子树链到f的右链上*/
free(p); /*释放被删除的结点p*/
}
else /*p有左子树*/
{
q=p;
s=p->lchild;
while(s->rchild) /*在p的左子树中查找最右下结点*/
{
q=s;
s=s->rchild;
}
if(q==p)
q->lchild=s->lchild ; /*将s的左子树链到q上*/
else
q->rchild=s->lchild;
p->key=s->key; /*将s的值赋给p*/
free(s);
}
return t;
} /*DelBST*/
void main()
{
BSTree T;
int k;
BSTree result;
printf("建立二叉排序树,请输入序列:\n");
CreateBST(&T);
printf("先序遍历输出序列为:");
PreOrder(T);
printf("\n请输入要查找的元素:");
fflush(stdin);
scanf("%d",&k);
result = SearchBST(T,k);
if (result != NULL)
printf("要查找的元素为%d\n",result->key);
else
printf("未找到!\n");
result = DelBST(T,k);
PreOrder(result);
}
复杂度分析:它和二分查找一样,插入和查找的时间复杂度均为O(logn),但是在最坏的情况下仍然会有O(n)的时间复杂度。原因在于插入和删除元素的时候,树没有保持平衡(比如,我们查找上图(b)中的“93”,我们需要进行n次查找操作)。我们追求的是在最坏的情况下仍然有较好的时间复杂度,这就是平衡查找树设计的初衷。
6、平衡二叉树(AVL树)
性质:
(1)一棵空树是平衡二叉树
(2)如果树不为空,它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。
(3)是一种二叉排序树
平衡因子BF: 对于任意节点 x ,其平衡因子定义为该节点右子树和左子树高度差,即 bf(x)=h(x-left)-h(x-right)。
平衡二叉树上所有结点的平衡因子只可能是-1、0、1
只要二叉树上有一个结点的平衡因子的绝对值大于1,则该二叉树就不是平衡的
最小不平衡子树:距离插入结点最近的,且平衡因子的绝对值大于1的结点为根的子树
1. AVL树数据结构
为了方便计算每个节点的平衡因子,对二叉树的数据结构进行修改,增加一个数据单元用于记录以该节点为root的子树高度,重新定义数据结构如下:
- struct BinaryTreeNode {
- keyType key;
- int height; //记录以该节点为root的树高度
- BinaryTreeNode* left; // left child
- BinaryTreeNode* right; // right child
- };
- // define AVL node
- typedef BinaryTreeNode avlnode;
- // define AVL tree
- typedef BinaryTreeNode avltree;
- // 比较左右子树高度,取最大的
- int maxh(int ha, int hb) {
- return ha > hb ? ha : hb;
- }
- // 计算树的高度
- int height(avltree* tree) {
- if (NULL == tree) return 0;
- return tree->height;
- }
2. AVL树旋转操作
AVL在插入和删除节点造成不平衡的时候需要对发生不平衡的节点及时调整,调整方法为旋转操作。根据造成不平衡的节点出构型可分为:LL 、RR 、LR 、RL型,对应的操作则为单旋和双旋,下面分析一下各种构型具体操作方法。
旋转规律:当最小不平衡子树根结点的平衡因子BF是大于1时,就对最小不平衡子树 右旋
小于-1时,就对最小不平衡子树 左旋
插入结点后,最小不平衡子树的BF与它的子树的BF符号相反时,就需要对它的子树先进行一次旋转(与BF符号对应)以使得符号相同后,再反向旋转一次才能够完成平衡操作。
单右旋:
/*对以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指向新的树根结点,即旋转处理之前的右子树的根结点*/
void L_Rotate(BiTree *p)
{
BiTree R;
R = (*p)->rchild;//L指向P的右子树根结点
(*p)->rchild = R->lchild;//R的左子树挂接为P的右子树
R->Lchild = (*p);
*p =R;//p指向新的根结点
}
双旋(先左后右):
void LR_Rotate(BiTree *p)
{
L_Rotate(&(*p)->lchild);
R_Rotate(p);
}
双旋(先右后左):
void RL_Rotate(BiTree *p)
{
L_Rotate(&(*p)->rchild);
R_Rotate(p);
}