查找表的概念
查找表是由同一類型的數據元素構成的集合。例如電話號碼簿和字典都可以看作是一張查找表。
在查找表中只做查找操作,而不改動表中數據元素,稱此類查找表爲靜態查找表;反之,在查找表中做查找操作的同時進行插入數據或者刪除數據的操作,稱此類表爲動態查找表。
順序查找
順序查找的查找過程爲:從表中的最後一個數據元素開始,逐個同記錄的關鍵字做比較,如果匹配成功,則查找成功;反之,如果直到表中第一個關鍵字查找完也沒有成功匹配,則查找失敗
同時,在程序中初始化創建查找表時,由於是順序存儲,所以將所有的數據元素存儲在數組中,但是把第一個位置留給了用戶用於查找的關鍵字。例如,在順序表{1,2,3,4,5,6}中查找數據元素值爲 7 的元素,則添加後的順序表爲:
圖1
順序表的一端添加用戶用於搜索的關鍵字,稱作“監視哨”。
圖 1 中監視哨的位置也可放在數據元素 6 的後面(這種情況下,整個查找的順序應有逆向查找改爲順序查找)。
放置好監視哨之後,順序表遍歷從沒有監視哨的一端依次進行,如果查找表中有用戶需要的數據,則程序輸出該位置;反之,程序會運行至監視哨,此時匹配成功,程序停止運行,但是結果是查找失敗。
代碼實現:
/*
* @Description: 順序查找算法
* @Version: V1.0
* @Autor: Carlos
* @Date: 2020-05-22 15:52:11
* @LastEditors: Carlos
* @LastEditTime: 2020-06-03 16:56:06
*/
#include <stdio.h>
#include <stdlib.h>
#define keyType int
typedef struct {
//查找表中每個數據元素的值
keyType key;
//如果需要,還可以添加其他屬性
}ElemType;
typedef struct{
//存放查找表中數據元素的數組
ElemType *elem;
//記錄查找表中數據的總數量
int length;
}SSTable;
/**
* @Description: 創建查找表
* @Param: SSTable **st 指向結構體指針的指針,即指針變量的指針,int length 創建的二叉樹的長度
* @Return: 無
* @Author: Carlos
*/
void Create(SSTable **st,int length){
(*st)=(SSTable*)malloc(sizeof(SSTable));
(*st)->length=length;
//結構體指針分配空間
(*st)->elem =(ElemType*)malloc((length+1)*sizeof(ElemType));
printf("輸入表中的數據元素:\n");
//根據查找表中數據元素的總長度,在存儲時,從數組下標爲 1 的空間開始存儲數據
for (int i=1; i<=length; i++) {
scanf("%d",&((*st)->elem[i].key));
}
}
/**
* @Description: 查找表查找的功能函數,其中key爲關鍵字
* @Param: SSTable *st指向結構體變量的指針,keyType key 要查找的元素
* @Return: key在查找表中的位置
* @Author: Carlos
*/
int Search_seq(SSTable *st,keyType key){
//將關鍵字作爲一個數據元素存放到查找表的第一個位置,起監視哨的作用
st->elem[0].key=key;
int i=st->length;
//從查找表的最後一個數據元素依次遍歷,一直遍歷到數組下標爲0
while (st->elem[i].key!=key) {
i--;
}
//如果 i=0,說明查找失敗;反之,返回的是含有關鍵字key的數據元素在查找表中的位置
return i;
}
int main(int argc, const char * argv[]) {
SSTable *st;
Create(&st, 6);
getchar();
printf("請輸入查找數據的關鍵字:\n");
int key;
scanf("%d",&key);
int location=Search_seq(st, key);
if (location==0) {
printf("查找失敗");
}else{
printf("數據在查找表中的位置爲:%d",location);
}
return 0;
}
折半查找
折半查找,也稱二分查找,在某些情況下相比於順序查找,使用折半查找算法的效率更高。但是該算法的使用的前提是靜態查找表中的數據必須是有序的。
例如,在{5,21,13,19,37,75,56,64,88 ,80,92}這個查找表使用折半查找算法查找數據之前,需要首先對該表中的數據按照所查的關鍵字進行排序:{5,13,19,21,37,56,64,75,80,88,92}。
在折半查找之前對查找表按照所查的關鍵字進行排序的意思是:若查找表中存儲的數據元素含有多個關鍵字時,使用哪種關鍵字做折半查找,就需要提前以該關鍵字對所有數據進行排序。
折半查找算法
對靜態查找表{5,13,19,21,37,56,64,75,80,88,92}採用折半查找算法查找關鍵字爲 21 的過程爲:
圖2
後一個關鍵字,指針 mid 指向處於 low 和 high 指針中間位置的關鍵字。在查找的過程中每次都同 mid 指向的關鍵字進行比較,由於整個表中的數據是有序的,因此在比較之後就可以知道要查找的關鍵字的大致位置。
例如在查找關鍵字 21 時,首先同 56 作比較,由於21 < 56,而且這個查找表是按照升序進行排序的,所以可以判定如果靜態查找表中有 21 這個關鍵字,就一定存在於 low 和 mid 指向的區域中間。
因此,再次遍歷時需要更新 high 指針和 mid 指針的位置,令 high 指針移動到 mid 指針的左側一個位置上,同時令 mid 重新指向 low 指針和 high 指針的中間位置。如圖3所示:
圖3
同樣,用 21 同 mid 指針指向的 19 作比較,19 < 21,所以可以判定 21 如果存在,肯定處於 mid 和 high 指向的區域中。所以令 low 指向 mid 右側一個位置上,同時更新 mid 的位置。
圖4
當第三次做判斷時,發現 mid 就是關鍵字 21 ,查找結束。
注意:在做查找的過程中,如果 low 指針和 high 指針的中間位置在計算時位於兩個關鍵字中間,即求得 mid 的位置不是整數,需要統一做取整操作。
折半查找的實現代碼:
/*
* @Description: 折半查找.前提是靜態查找表中的數據必須是有序的。
* @Version: V1.0
* @Autor: Carlos
* @Date: 2020-05-22 16:09:01
* @LastEditors: Carlos
* @LastEditTime: 2020-06-03 16:58:14
*/
#include <stdio.h>
#include <stdlib.h>
#define keyType int
typedef struct {
//查找表中每個數據元素的值
keyType key;
//如果需要,還可以添加其他屬性
}ElemType;
typedef struct{
//存放查找表中數據元素的數組
ElemType *elem;
//記錄查找表中數據的總數量
int length;
}SSTable;
/**
* @Description: 創建查找表
* @Param: SSTable **st 指向結構體指針的指針,即指針變量的指針,int length 創建的二叉樹的長度
* @Return: 無
* @Author: Carlos
*/
void Create(SSTable **st,int length){
(*st)=(SSTable*)malloc(sizeof(SSTable));
(*st)->length=length;
(*st)->elem = (ElemType*)malloc((length+1)*sizeof(ElemType));
printf("輸入表中的數據元素:\n");
//根據查找表中數據元素的總長度,在存儲時,從數組下標爲 1 的空間開始存儲數據
for (int i=1; i<=length; i++) {
scanf("%d",&((*st)->elem[i].key));
}
}
//折半查找算法
/**
* @Description: 折半查找算法
* @Param: SSTable *ST 指向結構體的指針,keyType key 要插入的元素
* @Return: 成功的返回key在查找表中的位置,否則返回0
* @Author: Carlos
*/
int Search_Bin(SSTable *ST,keyType key){
//初始狀態 low 指針指向第一個關鍵字
int low=1;
//high 指向最後一個關鍵字
int high=ST->length;
int mid;
while (low<=high) {
//int 本身爲整形,所以,mid 每次爲取整的整數
mid=(low+high)/2;
//如果 mid 指向的同要查找的相等,返回 mid 所指向的位置
if (ST->elem[mid].key==key)
{
return mid;
}
//如果mid指向的關鍵字較大,則更新 high 指針的位置
else if(ST->elem[mid].key>key)
{
high=mid-1;
}
//反之,則更新 low 指針的位置
else{
low=mid+1;
}
}
return 0;
}
int main(int argc, const char * argv[]) {
SSTable *st;
Create(&st, 11);
getchar();
printf("請輸入查找數據的關鍵字:\n");
int key;
scanf("%d",&key);
int location=Search_Bin(st, key);
//如果返回值爲 0,則證明查找表中未查到 key 值,
if (location==0) {
printf("查找表中無該元素");
}else{
printf("數據在查找表中的位置爲:%d",location);
}
return 0;
}
二叉查找樹
動態查找表中做查找操作時,若查找成功可以對其進行刪除;如果查找失敗,即表中無該關鍵字,可以將該關鍵字插入到表中。
動態查找表的表示方式有多種,本節介紹一種使用樹結構表示動態查找表的實現方法——二叉排序樹(又稱爲“二叉查找樹”)。
二叉查找樹概念
二叉排序樹要麼是空二叉樹,要麼具有如下特點:
- 二叉排序樹中,如果其根結點有左子樹,那麼左子樹上所有結點的值都小於根結點的值;
- 二叉排序樹中,如果其根結點有右子樹,那麼右子樹上所有結點的值都大小根結點的值;
- 二叉排序樹的左右子樹也要求都是二叉排序樹;
例如,圖 5 就是一個二叉排序樹:
圖5
使用二叉排序樹查找關鍵字
二叉排序樹中查找某關鍵字時,查找過程類似於次優二叉樹,在二叉排序樹不爲空樹的前提下,首先將被查找值同樹的根結點進行比較,會有 3 種不同的結果:
- 如果相等,查找成功;
- 如果比較結果爲根結點的關鍵字值較大,則說明該關鍵字可能存在其左子樹中;
- 如果比較結果爲根結點的關鍵字值較小,則說明該關鍵字可能存在其右子樹中;
實現函數爲:(運用遞歸的方法)
/**
* @Description: 二叉排序樹查找算法
* @Param: BiTree T KeyType key BiTree f BiTree *p
* @Return: 刪除成功 TRUE 刪除失敗 FALSE
* @Author: Carlos
*/
int SearchBST(BiTree T, KeyType key, BiTree f, BiTree *p)
{
//如果 T 指針爲空,說明查找失敗,令 p 指針指向查找過程中最後一個葉子結點,並返回查找失敗的信息
if (!T)
{
*p = f;
return FALSE;
}
//如果相等,令 p 指針指向該關鍵字,並返回查找成功信息
else if (key == T->data)
{
*p = T;
return TRUE;
}
//如果 key 值比 T 根結點的值小,則查找其左子樹;反之,查找其右子樹
else if (key < T->data)
{
return SearchBST(T->lchild, key, T, p);
}
else
{
return SearchBST(T->rchild, key, T, p);
}
}
二叉排序樹中插入關鍵字
二叉排序樹本身是動態查找表的一種表示形式,有時會在查找過程中插入或者刪除表中元素,當因爲查找失敗而需要插入數據元素時,該數據元素的插入位置一定位於二叉排序樹的葉子結點,並且一定是查找失敗時訪問的最後一個結點的左孩子或者右孩子。
例如,在圖 1 的二叉排序樹中做查找關鍵字 1 的操作,當查找到關鍵字 3 所在的葉子結點時,判斷出表中沒有該關鍵字,此時關鍵字 1 的插入位置爲關鍵字 3 的左孩子。
所以,二叉排序樹表示動態查找表做插入操作,只需要稍微更改一下上面的代碼就可以實現,具體實現代碼爲:
/**
* @Description: 二叉排序樹查找算法
* @Param: BiTree T KeyType key BiTree f BiTree *p
* @Return: 刪除成功 TRUE 刪除失敗 FALSE
* @Author: Carlos
*/
int SearchBST(BiTree T, KeyType key, BiTree f, BiTree *p)
{
//如果 T 指針爲空,說明查找失敗,令 p 指針指向查找過程中最後一個葉子結點,並返回查找失敗的信息
if (!T)
{
*p = f;
return FALSE;
}
//如果相等,令 p 指針指向該關鍵字,並返回查找成功信息
else if (key == T->data)
{
*p = T;
return TRUE;
}
//如果 key 值比 T 根結點的值小,則查找其左子樹;反之,查找其右子樹
else if (key < T->data)
{
return SearchBST(T->lchild, key, T, p);
}
else
{
return SearchBST(T->rchild, key, T, p);
}
}
/**
* @Description: 二叉樹插入函數
* @Param: BiTree *T 二叉樹結構體指針的指針 ElemType e 要插入的元素
* @Return: 刪除成功 TRUE 刪除失敗 FALSE
* @Author: Carlos
*/
int InsertBST(BiTree *T, ElemType e)
{
BiTree p = NULL;
//如果查找不成功,需做插入操作
if (!SearchBST((*T), e, NULL, &p))
{
//初始化插入結點
BiTree s = (BiTree)malloc(sizeof(BiTree));
s->data = e;
s->lchild = s->rchild = NULL;
//如果 p 爲NULL,說明該二叉排序樹爲空樹,此時插入的結點爲整棵樹的根結點
if (!p)
{
*T = s;
}
//如果 p 不爲 NULL,則 p 指向的爲查找失敗的最後一個葉子結點,只需要通過比較 p 和 e 的值確定 s 到底是 p 的左孩子還是右孩子
else if (e < p->data)
{
p->lchild = s;
}
else
{
p->rchild = s;
}
return TRUE;
}
//如果查找成功,不需要做插入操作,插入失敗
return FALSE;
}
通過使用二叉排序樹對動態查找表做查找和插入的操作,同時在中序遍歷二叉排序樹時,可以得到有關所有關鍵字的一個有序的序列。
例如,假設原二叉排序樹爲空樹,在對動態查找表 {3,5,7,2,1} 做查找以及插入操作時,可以構建出一個含有表中所有關鍵字的二叉排序樹,過程如圖6 所示:
圖6
通過不斷的查找和插入操作,最終構建的二叉排序樹如圖 6(5) 所示。當使用中序遍歷算法遍歷二叉排序樹時,得到的序列爲:1 2 3 5 7 ,爲有序序列。
一個無序序列可以通過構建一棵二叉排序樹,從而變成一個有序序列。
二叉排序樹中刪除關鍵字
在查找過程中,如果在使用二叉排序樹表示的動態查找表中刪除某個數據元素時,需要在成功刪除該結點的同時,依舊使這棵樹爲二叉排序樹。
假設要刪除的爲結點 p,則對於二叉排序樹來說,需要根據結點 p 所在不同的位置作不同的操作,有以下 3 種可能:
- 結點 p 爲葉子結點,此時只需要刪除該結點,並修改其雙親結點的指針即可;
- 結點 p 只有左子樹或者只有右子樹,此時只需要將其左子樹或者右子樹直接變爲結點 p 雙親結點的左子樹即可;
- 結點 p 左右子樹都有,此時有兩種處理方式:
(1).令結點 p 的左子樹爲其雙親結點的左子樹;結點 p 的右子樹爲其自身直接前驅結點的右子樹,如圖7所示;
圖7
(2)用結點 p 的直接前驅(或直接後繼)來代替結點 p,同時在二叉排序樹中對其直接前驅(或直接後繼)做刪除操作。如圖 8 爲使用直接前驅代替結點 p:
圖8
圖 8中,在對左圖進行中序遍歷時,得到的結點 p 的直接前驅結點爲結點 s,所以直接用結點 s 覆蓋結點 p,由於結點 s 還有左孩子,根據第 2 條規則,直接將其變爲雙親結點的右孩子。
具體實現代碼:(可運行)
/*
* @Description: 二叉查找樹
* @Version: V1.0
* @Autor: Carlos
* @Date: 2020-06-02 15:50:31
* @LastEditors: Carlos
* @LastEditTime: 2020-06-03 16:49:46
*/
#include <stdio.h>
#include <stdlib.h>
#define TRUE 1
#define FALSE 0
#define ElemType int
#define KeyType int
/* 二叉排序樹的節點結構定義 */
typedef struct BiTNode
{
int data;
struct BiTNode *lchild, *rchild;
} BiTNode, *BiTree;
/**
* @Description: 二叉排序樹查找算法
* @Param: BiTree T KeyType key BiTree f BiTree *p
* @Return: 刪除成功 TRUE 刪除失敗 FALSE
* @Author: Carlos
*/
int SearchBST(BiTree T, KeyType key, BiTree f, BiTree *p)
{
//如果 T 指針爲空,說明查找失敗,令 p 指針指向查找過程中最後一個葉子結點,並返回查找失敗的信息
if (!T)
{
*p = f;
return FALSE;
}
//如果相等,令 p 指針指向該關鍵字,並返回查找成功信息
else if (key == T->data)
{
*p = T;
return TRUE;
}
//如果 key 值比 T 根結點的值小,則查找其左子樹;反之,查找其右子樹
else if (key < T->data)
{
return SearchBST(T->lchild, key, T, p);
}
else
{
return SearchBST(T->rchild, key, T, p);
}
}
/**
* @Description: 二叉樹插入函數
* @Param: BiTree *T 二叉樹結構體指針的指針 ElemType e 要插入的元素
* @Return: 刪除成功 TRUE 刪除失敗 FALSE
* @Author: Carlos
*/
int InsertBST(BiTree *T, ElemType e)
{
BiTree p = NULL;
//如果查找不成功,需做插入操作
if (!SearchBST((*T), e, NULL, &p))
{
//初始化插入結點
BiTree s = (BiTree)malloc(sizeof(BiTree));
s->data = e;
s->lchild = s->rchild = NULL;
//如果 p 爲NULL,說明該二叉排序樹爲空樹,此時插入的結點爲整棵樹的根結點
if (!p)
{
*T = s;
}
//如果 p 不爲 NULL,則 p 指向的爲查找失敗的最後一個葉子結點,只需要通過比較 p 和 e 的值確定 s 到底是 p 的左孩子還是右孩子
else if (e < p->data)
{
p->lchild = s;
}
else
{
p->rchild = s;
}
return TRUE;
}
//如果查找成功,不需要做插入操作,插入失敗
return FALSE;
}
/**
* @Description: 刪除節點的函數
* @Param: BiTree *p 指向結構體指針的指針
* @Return: 刪除成功 TRUE
* @Author: Carlos
*/
int Delete(BiTree *p)
{
BiTree q, s;
//情況 1,結點 p 本身爲葉子結點,直接刪除即可
if (!(*p)->lchild && !(*p)->rchild)
{
*p = NULL;
}
//左子樹爲空,只需用結點 p 的右子樹根結點代替結點 p 即可;
else if (!(*p)->lchild)
{
q = *p;
*p = (*p)->rchild;
free(q);
q = NULL;
}
//右子樹爲空,只需用結點 p 的左子樹根結點代替結點 p 即可;
else if (!(*p)->rchild)
{
q = *p;
//這裏不是指針 *p 指向左子樹,而是將左子樹存儲的結點的地址賦值給指針變量 p
*p = (*p)->lchild;
free(q);
q = NULL;
}
//左右子樹均不爲空,採用第 2 種方式
else
{
q = *p;
s = (*p)->lchild;
//遍歷,找到結點 p 的直接前驅
while (s->rchild)
{
//指向p節點左子樹最右邊節點的前一個。保留下來
q = s;
s = s->rchild;
}
//直接改變結點 p 的值
(*p)->data = s->data;
//判斷結點 p 的左子樹 s 是否有右子樹,分爲兩種情況討論 如果有右子樹,s一定會指向右子樹的葉子節點。q 此時指向的是葉子節點的父節點。 q != *p二者不等說明有右子樹
if (q != *p)
{
//若有,則在刪除直接前驅結點的同時,令前驅的左孩子結點改爲 q 指向結點的孩子結點
q->rchild = s->lchild;
}
else
//q == *p ==NULL 說明沒有右子樹
{
//否則,直接將左子樹上移即可
q->lchild = s->lchild;
}
free(s);
s = NULL;
}
return TRUE;
}
/**
* @Description: 刪除二叉樹中的元素
* @Param: BiTree *T 指向二叉樹結構體的指針 int key 要刪除的元素
* @Return: 刪除成功 TRUE 刪除失敗 FALSE
* @Author: Carlos
*/
int DeleteBST(BiTree *T, int key)
{
if (!(*T))
{ //不存在關鍵字等於key的數據元素
return FALSE;
}
else
{
if (key == (*T)->data)
{
Delete(T);
return TRUE;
}
else if (key < (*T)->data)
{
//使用遞歸的方式
return DeleteBST(&(*T)->lchild, key);
}
else
{
return DeleteBST(&(*T)->rchild, key);
}
}
}
/**
* @Description: 中序遍歷輸出二叉樹
* @Param: BiTree t 結構體變量
* @Return: 無
* @Author: Carlos
*/
void order(BiTree t)
{
if (t == NULL)
{
return;
}
order(t->lchild);
printf("%d ", t->data);
order(t->rchild);
}
int main()
{
int i;
int a[5] = {3, 4, 2, 5, 9};
BiTree T = NULL;
for (i = 0; i < 5; i++)
{
InsertBST(&T, a[i]);
}
printf("中序遍歷二叉排序樹:\n");
order(T);
printf("\n");
printf("刪除3後,中序遍歷二叉排序樹:\n");
DeleteBST(&T, 3);
order(T);
}