1. 靜態查找
靜態查找是“真正的查找”。因爲在靜態查找過程中僅僅是執行“查找”的操作,即:
(1)查看某特定的關鍵字是否在表中(判斷性查找)
(2)檢索某特定關鍵字數據元素的各種屬性(檢索性查找)。
這兩種操作都只是獲取已經存在的一個表中的數據信息,不對錶的數據元素和結構進行任何改變,這就是所謂的靜態查找。
常見的靜態查找:順序查找、插值查找、二分查找、裴波拉契查找
1.1 順序查找
順序查找,又稱爲線性查找。是最基本的查找技術。其查找過程:從表中的第一個或者最後一個記錄開始,逐個比較關鍵字和給定值。若相等,則查找成功,找到所查記錄;如果查找完,關鍵字和給定值都不相等,則沒有所查記錄。
代碼實現:
// a爲數組,n爲查找的數組個數,key爲要查找的關鍵字
int Sequential_Search(int *a,int n,int key){
for (int i = 1; i <= n ; i++)
if (a[i] == key)
return i;
return 0;
}
在順序查找時,我們可以對其添加哨兵,數組a
的第0個位置作爲哨兵,來存儲查找的值
int Sequential_Search2(int *a,int n,int key){
int i;
// ✅設置a[0]爲關鍵字值,稱爲'哨兵'
a[0] = key;
// ✅循環從數組尾部開始
i = n;
while (a[i] != key) {
i--;
}
// 返回0,則說明查找失敗
return i;
}
1.2 折半查找
折半查找又叫二分查找,也是常有的查找算法。
折半查找的前提是線性表中的記錄必須是關鍵碼有序(通常是從小到大),表必須採用順序存儲。
折半查找思路:
- 在有序表中,去中間記錄作爲比較對象,若給定值與中間記錄的關鍵字相等則查找成功
- 若小於中間記錄的關鍵字,則在中間記錄的左半區繼續查找
- 若給定值大於中間記錄的關鍵字,則在中間記錄的右半區繼續查找
- 不斷重複以上過程,直到查找成功,或者所有查找區域無記錄,則查找失敗未知。
// 假設 數組a,從小到大有序
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]) {
// ✅若key比a[mid] 小,則將最高下標調整到中位下標小一位;
high = mid-1;
}else if(key > a[mid]){
// ✅若key比a[mid] 大,則將最低下標調整到中位下標大一位;
low = mid+1;
}else
// ✅若相等則說明mid即爲查找到的位置;
return mid;
}
return 0;
}
1.3 插值查找
假設數據a[11] = {0, 1, 16, 24, 35, 47, 59, 62, 73, 88, 99},有折半查找key = 16
時,low = 1
,height = 10
,則a[low] = 1
,a[height] = 99
,需要查找四次
折半查找的公式如下:
我們對其優化,mid
等於最低下標low
加上最高下標height
與最低下標low
差值的一半。
然後將1/2改成下面的形式
那麼,
對2.377
取整,則mid = 2
,我們只需要查找兩次就能得到結果。這就是插值查找。
int Interpolation_Search(int *a,int n,int key){
int low,high,mid;
low = 1;
high = n;
while (low <= high) {
//插值
mid = low+ (high-low)*(key-a[low])/(a[high]-a[low]);
if (key < a[mid]) {
// ✅若key比a[mid]插值小,則將最高下標調整到插值下標小一位;
high = mid-1;
}else if(key > a[mid]){
// ✅若key比a[mid]插值 大,則將最低下標調整到插值下標大一位;
low = mid+1;
}else
//若相等則說明mid即爲查找到的位置;
return mid;
}
return 0;
}
1.4 裴波拉契查找
裴波拉契查找需要依靠裴波拉契數列裴波拉契
裴波拉契查找公式:
- mid = low + F[k-1] - 1
- key < a[mid],
k = k - 1
- key > a[mid],
k = k - 2
假設有下面的一個數列:
F爲裴波拉契數列,假設查找 n = 10,k = 99
首先n = 10
, F[6] < n < F[7]
,,所以計算得出k = 7
。 找出n
位於斐波那契數列的位置。
F[7] = 13
, 而a
最大僅有a[10]
。後面的a[11]
,a[12]
是未賦值。不能構成有序數列。所以將後續的2
個元素賦值 a[11] = a[12] = a[10] = 99
。
-
第一次查找
low = 1,k = 7
,
mid = low + F[k-1] - 1 = 1 + F[7-1] - 1 = 1 + 8 - 1 = 8
key > a[8] (99 > 73)
k = k - 2 = 7 - 2 = 5
low = mid + 1 = 9
-
第二次查找
low = 9,k = 5
,
mid = low + F[k-1] - 1 = 9 + F[5-1] - 1 = 9 + 3 - 1 = 11
key > a[11] (99 = 99)
mid > n
,11>10
,則返回n
。所以返回10
表示找到key =99
在數組a
中的位置,在10
這個位置
思路:
1. 先計算n位於斐波拉契數列的位置
2. 將數組a不滿的位置補全值
3. 循環 low <= high
4. mid = low+F[k-1]-1;
4.1 key < a[mid],high = mid-1;k = k - 1
4.2 key > a[mid],low = mid+1;k = k - 2
4.3 判斷 mid <= n,返回 mid 或者 n
代碼實現:
int F[100]; /* 斐波那契數列 */
int Fibonacci_Search(int *a,int n,int key){
int low,high,mid,i,k;
//最低下標爲記錄的首位;
low = 1;
//最高下標爲記錄的末位;
high = n;
k = 0;
//1.計算n爲斐波拉契數列的位置;
while (n > F[k]-1) {
k++;
}
//2.將數組a不滿的位置補全值;
for(i = n;i < F[k]-1;i++)
a[i] = a[n];
//3.
while (low <= high) {
//計算當前分隔的下標;
mid = low+F[k-1]-1;
if (key < a[mid]) {
//若查找的記錄小於當前分隔記錄;
//將最高下標調整到分隔下標mid-1處;
high = mid-1;
//斐波拉契數列下標減1位;
k = k-1;
}else if(key > a[mid]){
//若查找的記錄大於當前的分隔記錄;
//最低下標調整到分隔下標mid+1處
low = mid+1;
//斐波拉契數列下標減2位;
k = k-2;
}else{
if (mid <= n) {
//若相等則說明,mid即爲查找的位置;
return mid;
}else
{
//若mid>n,說明是補全數值,返回n;
return n;
}
}
}
return 0;
}
2. 動態查找(二叉搜索樹)
動態查找:它更像是一個對錶進行創建、擴充、修改、刪除的過程。
動態查找的過程中對錶的操作會多兩個動作:
(1)首先也有一個“判斷性查找”的過程,如果某特定的關鍵字在表中不存在,則按照一定的規則將其插入表中;
(2)如果已經存在,則可以對其執行刪除操作。
常見的動態查找(表):各種樹(二叉搜索樹、AVL、B/B+樹、紅黑樹等等)、哈希表
接下來來了解一下二叉排序樹,假設有以下一組數a[62,88,58,47,35,73,51,99,37,93]。當我們用順序存儲的線性表進行存儲時,假設要查找key = 93
時,要遍歷很多次才能找到。
那麼嗎,我們藉助二叉樹來存儲,在存儲的過程中,比雙親節點小的,存儲到左子樹,比雙親節點大的,存儲到右子樹,可以通過比較縮小查找範圍,其實這樣存儲的二叉樹就是二叉排序樹。
二叉排序樹又稱爲二叉查找樹,它或者是一個空樹,或者具有以下性質的二叉樹:
- 若左子樹不爲空,則左子樹上所以的節點均小於其根結構的值
- 若右子樹不爲空,則右子樹上所以的節點均小於其根結構的值
- 其左右子樹分別是二叉排序樹
對上面的數組,以二叉排序樹的形式存儲,如下:
2.1 查找數據
假設查找key = 93
,根據上圖,則:
- 第一次,62 < 93,縮小查找範圍到其右子樹
- 第二次,88 < 93,縮小查找範圍到其右子樹
- 第三次,99 > 93,縮小查找範圍到其左子樹
- 第四次,93 = 93,成功,返回
true
那麼代碼怎麼實現呢?先來定義一下二叉樹的二叉鏈表結點結構
#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
#define MAXSIZE 100
typedef int Status;
//結點結構
typedef struct BiTNode
{
//結點數據
int data;
//左右孩子指針
struct BiTNode *lchild, *rchild;
} BiTNode, *BiTree;
查找思想:
利用遞歸,比較key 和 節點data的值,相等,則返回true,
大於,則遞歸右子樹,
小於,則遞歸左子樹
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); /* 在右子樹中繼續查找 */
}
2.2 插入數據
當向二叉搜索樹中插入某個key
時,要先進行查找
插入思路:
1. 先查找插入的值,是否存在二叉樹中,存在,則插入失敗
2. 不存在,則插入到合適的位置
2.1 初始化節點,並用 key 對其 data 賦值,左右子樹爲 NULL
2.2 通過最後查找返回的節點 P進行比較,
key < p,新節點插入爲左孩子
key > p,新節點插入爲右孩子
Status InsertBST(BiTree *T, int key) {
BiTree p,s;
//✅ 查找插入的值是否存在二叉樹中;查找失敗則->
if (!SearchBST(*T, key, NULL, &p)) {
//✅ 初始化結點s,並將key賦值給s,將s的左右孩子結點暫時設置爲NULL
s = (BiTree)malloc(sizeof(BiTNode));
s->data = key;
s->lchild = s->rchild = NULL;
//✅ 比較,插入
if (!p) {
//如果p爲空,則將s作爲二叉樹新的根結點;
*T = s;
}else if(key < p->data){
//如果key<p->data,則將s插入爲左孩子;
p->lchild = s;
}else
//如果key>p->data,則將s插入爲右孩子;
p->rchild = s;
return TRUE;
}
// ✅ 查找到,則返回插入失敗
return FALSE;
}
2.3 刪除數據
二叉搜索樹的刪除,可以分爲三種情況:
- 刪除的數據是葉子節點,比如上圖中的
37,51,73,93
,這樣的節點直接刪除即可,不會影響二叉搜索樹的結構 - 刪除的數據只有左子樹或者右子樹,比如
58,35,99
,先刪除,然後在將其左子樹或者右子樹,連接到被刪除節點的雙親節點的左子樹或者右子樹上。 - 刪除的數據,既有左子樹,又有右子樹,比如
47
。
那麼第三種這樣的節點怎麼刪除呢?我們來分析一下:
1. 定義兩個變量temp 和 p 都指向 被刪除節點 47.
2. 定義 s 指向待刪除節點的左子樹
3. 在待刪除節點的左子樹中,從右邊找到直接前驅(中序遍歷 29 , 35,37,47---)
4. 用 temp 保存好直接前驅的雙親節點
此時如下圖:
5. 將要刪除節點的 p 的數據賦值成 s->data,即:將37 賦值給到 p,替換 47
6. 判斷,如果 temp 不等於 p, 將 s 的左子樹賦值給 temp 的右子樹
7. 判斷,如果 temp 等於 p, 將 s 的左子樹賦值給 temp 的左子樹
8. 釋放 s 指向的節點
最終如下:
代碼實現:
Status Delete(BiTree *p){
BiTree temp,s;
if((*p)->rchild == NULL){
//✅ 情況1: 如果當前刪除的結點,右子樹爲空.那麼則只需要重新連接它的左子樹(刪除葉子節點時,其左右孩子爲NULL)
//✅ 將結點p臨時存儲到temp中;
temp = *p;
//✅ 將p指向到p的左子樹上;
*p = (*p)->lchild;
//✅ 釋放需要刪除的temp結點;
free(temp);
}else if((*p)->lchild == NULL){
//✅ 情況2:如果當前刪除的結點,左子樹爲空.那麼則只需要重新連接它的右子樹(刪除葉子節點時,其左右孩子爲NULL)
//✅ 將結點p存儲到temp中;
temp = *p;
//✅ 將p指向到p的右子樹上;
*p = (*p)->rchild;
//✅ 釋放需要刪除的temp結點
free(temp);
}else{
//✅ 情況③:刪除的當前結點的左右子樹均不爲空;
//✅ 1. 將結點p存儲到臨時變量temp, 並且讓結點s指向p的左子樹
temp = *p;
s = (*p)->lchild;
//✅ 2. 將s指針,向右到盡頭(目的是找到待刪結點的前驅)
//-在待刪除的結點的左子樹中,從右邊找到直接前驅
//-使用`temp`保存好直接前驅的雙親結點
while (s->rchild) {
temp = s;
s = s->rchild;
}
//✅ 3. 將要刪除的結點p數據賦值成s->data;
(*p)->data = s->data;
//✅ 4. 重連子樹
//✅ 如果temp 不等於p,則將S->lchild 賦值給temp->rchild
//✅ 如果temp 等於p,則將S->lchild 賦值給temp->lchild
if(temp != *p)
temp->rchild = s->lchild;
else
temp->lchild = s->lchild;
//✅ 5. 刪除s指向的結點; free(s)
free(s);
}
return TRUE;
}
//4.查找結點,並將其在二叉排序中刪除;
/* 若二叉排序樹T中存在關鍵字等於key的數據元素時,則刪除該數據元素結點, */
/* 並返回TRUE;否則返回FALSE。 */
Status DeleteBST(BiTree *T,int key)
{
//✅ 1. 不存在關鍵字等於key的數據元素
if(!*T)
return FALSE;
else
{
//✅ 2. 找到關鍵字等於key的數據元素
if (key==(*T)->data)
return Delete(T);
else if (key<(*T)->data)
//✅ 3. 關鍵字key小於當前結點,則縮小查找範圍到它的左子樹;
return DeleteBST(&(*T)->lchild,key);
else
//✅ 3. 關鍵字key大於當前結點,則縮小查找範圍到它的右子樹;
return DeleteBST(&(*T)->rchild,key);
}
}