順序查找(線性查找)
1、基礎內容
順序查找(Sequential Search)又叫線性查找,是最基本的查找技術。它的查找過程為:從表中第一個(或最后一個)記錄開始,逐個進行記錄的關鍵字和給定值比較,如果某個記錄的關鍵字和給定值相等,則查找成功,找到所查的記錄。如果直到最后一個(或第一個)記錄,其關鍵字和給定值比較都不等時,則表中沒有所查的記錄,查找不成功。
int Sequential_Search(int *a,int n,int key) {//a為數組名,n為數組長度,key為要查找的關鍵字 int i; for(i = 0;i < n;++i) { if(a[i] == key) return i; } return 0; }
2、優化
上面的程序每次循環都需要判斷數組是否越界,可通過設置哨兵的方法而不必每次進行比較。在查找方向的盡頭放置“哨兵”免去了在查找過程中每一次比較后都要判斷查找位置是否越界的小技巧。在總數據較多時,效率卻提高很大。
int Sequential_Search(int *a,int n,int key)//數組定義長度為a[n+1] { int a[0] = key;//哨兵 int i = n; while(a[i] != key) i--; //對於內置數據類型,--i和i--的效率一樣,而對於自定義類型(類)的重載--運算符函數效率不同。(前者效率更高。) return i; //返回0表示沒有匹配的值。查找失敗必然會返回0(以為a[0]值即為key,跳出循環) }
3、分析
顯然可知該算法的時間復雜度為O(n)。當n很大時,查找效率極為低下。用於小型數據的查找可以體現出算法簡單的優點。
有序表查找
1、折半查找
折半查找(Binary Search) 又稱為 二分查找。
基本思想:
- 首先,假設表中元素是按升序排列,將表中間位置記錄的關鍵字與查找關鍵字比較,如果兩者相等,則查找成功;
- 否則利用中間位置記錄將表分成前、后兩個子表,如果中間位置記錄的關鍵字大於查找關鍵字,則進一步查找前一子表,否則進一步查找后一子表。
- 重復以上過程,直到找到滿足條件的記錄,使查找成功,或直到子表不存在為止,此時查找不成功。
int Binary_Search(int *a, int n, int key) { int low, high, mid; low = 0; high = n - 1; while (low<=high) { mid = low+(high-low) / 2;//怕high+low溢出,采用這個寫法 if (a[mid] < key) low = mid + 1; else if (a[mid]>key) high = mid - 1; else return mid; } return -1; }
2、插值查找
不一定非要按1/2處划分,於是改進如下:
int Binary_Search(int *a, int n, int key) { int low, high, mid; low = 0; high = n - 1; while (low<=high) { mid = low + (key - a[low]) / (a[high] - a[low])*(high - low);//只換了這句話 if (a[mid] < key) low = mid + 1; else if (a[mid]>key) high = mid - 1; else return mid; } return -1; }
3、斐波那契查找
斐波那契查找是利用黃金分隔原理來實現的。它的本質和二分查找、插值查找沒有區別,都是通過設置分隔,不斷將區間縮小,最后查找到關鍵字的。與之前兩個查找方法相似,斐波那契的不同也是分隔設置的不同,它是通過斐波那契數列來設置的。
斐波那契查找的一個限制就是,src的數據個數需要是斐波那契數列中的元素之一。如果不滿足,需要將數組按最后元素鏡像擴容。
補代碼
4、總結
- 時間復雜度:二分查找、插值查找和斐波那契查找的時間復雜度都是O(logn)。
- 二分查找的前提條件是需要有序表順序存儲,對於靜態查找表,一次排序后不再變化,這樣的算法已經比較好了。但是對於需要頻繁執行插入或者刪除操作的數據集來說,維護有序表的排序會帶來不小的工作量,並不適合使用。
- 插值查找,對於表長較大,而關鍵字分布比較均勻的查找表來說,插值查找算法的平均性能要比折半查找好得多。反之,如果數據中分布類似{0,1,9999,999999}這樣極端不均勻的數據,用插值查找未必是最合適的選擇。
- 斐波那契查找,就平均性能而言,要優於二分查找,但是如果是最壞的情況,比如key=0,那么始終在左側長半區在查找,查找的效率要低於折半查找。
- 比較關鍵的一點是,插值、折半都需要進行比較復雜的乘除法運算,而斐波那契只需要進行簡單的加減運算,在海量數據的查找過程中,這種細微的差別可能會影響最終的查找效率。
線性索引查找
索引:就是把一個關鍵字與它對應的i記錄相關聯的過程,一個索引由若干個索引項構成,每個索引項至少應包含關鍵字和其對應的記錄在存儲器中的位置等信息。
索引按照結構可以分為:線性索引、樹形索引和多級索引。
線性索引是將索引項集合組織為線性結構,也稱為索引表。包括稠密索引、分塊索引、倒排索引。
1、稠密索引
稠密索引是指在線性索引表中,將數據集中的每個記錄對應一個索引項。並且索引項一定是按照關鍵碼有序的排列。
優點:
索引項有序也就意味着,在查找關鍵字時,可以用到折半、插值、斐波那契等有序的查找算法。
缺點:
如果數據集非常大,比如說上億,那也就意味着索引同樣的數據規模,可能就需要反復查詢內存和硬盤,性能可能反而下降了。
補代碼
2、分塊索引
分塊有序需要滿足兩個條件:塊內無序(有序更好,就是代價比較大)、塊間有序
對於分塊有序的數據集,將每塊對應一個索引項,這種索引方法叫做分塊索引。索引項結構分為三個數據項:最大關鍵碼、塊長和塊首指針。
補代碼
3、倒排索引
例如我們看如下兩句話:
- Books and friends should be few but good
- A good book is a good friend
如圖所示,我們將單詞做了排序,也就是表格顯示了每個不同的單詞分別出現在哪篇文章中。
有了這張表,我們在查每個單詞時都能很快的查找到這個單詞在哪篇文章。
在這里這張單詞表就是索引表,索引項的通用結構是:
- 次關鍵碼,例如上表中的“英文單詞”
- 記錄號表,例如上表中的“文章編號”
其中記錄號表存儲具有相同次關鍵字的所有記錄的記錄號(可以是指向記錄的指針或者是該記錄的主關鍵字)。這樣的索引方法就是倒排索引。
補代碼
二叉排序樹
二叉排序樹,又叫二叉查找樹,它或者是一棵空樹;或者是具有以下性質的二叉樹:
- 1. 若它的左子樹不空,則左子樹上所有節點的值均小於它的根節點的值;
- 2. 若它的右子樹不空,則右子樹上所有節點的值均大於它的根節點的值;
- 3. 它的左右子樹也分別為二叉排序樹。
如下圖所示:
其中序遍歷得到序列是一個有序序列,所以稱為二叉排序樹。
1、實現查找(遞歸實現)
BSTNode* BSTree::search(BSTNode* node, int key) { if (node == NULL || node->key == key) return node; if (node->key < key) search(node->right, key); else search(node->left, key); }
2、插入(遞歸實現)
template<typename T> void BinaryTree<T>::Insert(const T &data, Node<T>* &b) //遞歸法在二叉排序樹中插入一個節點 { if (b == NULL) //遞歸終止條件 { b = new Node<T>(data); if (!b) { cout << "out of space!" << endl; } } else if (data < b->info) Insert(data, b->lchild); else Insert(data, b->rchild);
}
3、刪除
前兩種都好說,第三種的思想是,用該節點左孩子子樹的最大值,或者該節點右孩子的最小值(這兩個值和這個幾點最近),代替該節點位置,刪除原樹中該節點和被用來替換的節點。
補代碼
4、總結
給定值的比較次數等於給定值節點在二叉排序樹中的層數。如果二叉排序樹是平衡的,則n個節點的二叉排序樹的高度為Log2n+1,其查找效率為O(Log2n),近似於折半查找。如果二叉排序樹完全不平衡,則其深度可達到n,查找效率為O(n),退化為順序查找。一般的,二叉排序樹的查找性能在O(Log2n)到O(n)之間。因此,為了獲得較好的查找性能,就要構造一棵平衡的二叉排序樹。
平衡二叉樹(AVL樹)
1、概念
平衡二叉樹(Balanced Binary Tree)又被稱為AVL樹(有別於AVL算法),且具有以下性質:它是一 棵空樹或它的左右兩個子樹的高度差的絕對值不超過1,並且左右兩個子樹都是一棵平衡二叉樹。這個方案很好的解決了二叉查找樹退化成鏈表的問題,把插入,查找,刪除的時間復雜度最好情況和最壞情況都維持在O(logN)。但是頻繁旋轉會使插入和刪除犧牲掉O(logN)左右的時間,不過相對二叉查找樹來說,時間上穩定了很多。
平衡二叉樹大部分操作和二叉查找樹類似,主要不同在於插入刪除的時候平衡二叉樹的平衡可能被改變,並且只有從那些插入點到根結點的路徑上的結點的平衡性可能被改變,因為只有這些結點的子樹可能變化。
平衡二叉樹的遞歸定義:平衡二叉樹是一棵二叉樹,其可以為空,或滿足如下2個性質:①左右子樹深度之差的絕對值不大於1。②左右子樹都是平衡二叉樹。
平衡因子的概念:結點的平衡因子 = 結點的左子樹深度 — 結點的右子樹深度。若平衡因子的取值為-1、0或1時,該節點是平衡的,否則是不平衡的。
最低不平衡結點的概念:用A表示最低不平衡結點,則A的祖先結點可能有不平衡的,但其所有后代結點都是平衡的。
2.平衡化的實現
整個實現過程是通過在一棵平衡二叉樹中依次插入元素(按照二叉排序樹的方式),若出現不平衡,則要根據新插入的結點與最低不平衡結點的位置關系進行相應的調整。分為LL型、RR型、LR型和RL型4種類型,各調整方法如下(下面用A表示最低不平衡結點):
(1)LL型調整:
由於在A的左孩子(L)的左子樹(L)上插入新結點,使原來平衡二叉樹變得不平衡,此時A的平衡因子由1增至2。下面圖1是LL型的最簡單形式。顯然,按照大小關系,結點B應作為新的根結點,其余兩個節點分別作為左右孩子節點才能平衡,A結點就好像是繞結點B順時針旋轉一樣。
LL型調整的一般形式如下圖2所示,表示在A的左孩子B的左子樹BL(不一定為空)中插入結點(圖中陰影部分所示)而導致不平衡( h 表示子樹的深度)。這種情況調整如下:①將A的左孩子B提升為新的根結點;②將原來的根結點A降為B的右孩子;③各子樹按大小關系連接(BL和AR不變,BR調整為A的左子樹)。
(2)RR型調整:
由於在A的右孩子(R)的右子樹(R)上插入新結點,使原來平衡二叉樹變得不平衡,此時A的平衡因子由-1變為-2。圖3是RR型的最簡單形式。顯然,按照大小關系,結點B應作為新的根結點,其余兩個節點分別作為左右孩子節點才能平衡,A結點就好像是繞結點B逆時針旋轉一樣。
RR型調整的一般形式如下圖4所示,表示在A的右孩子B的右子樹BR(不一定為空)中插入結點(圖中陰影部分所示)而導致不平衡( h 表示子樹的深度)。這種情況調整如下:①將A的右孩子B提升為新的根結點;②將原來的根結點A降為B的左孩子;③各子樹按大小關系連接(AL和BR不變,BL調整為A的右子樹)。
(3)LR型調整:
由於在A的左孩子(L)的右子樹(R)上插入新結點,使原來平衡二叉樹變得不平衡,此時A的平衡因子由1變為2。圖5是LR型的最簡單形式。顯然,按照大小關系,結點C應作為新的根結點,其余兩個節點分別作為左右孩子節點才能平衡。
LR型調整的一般形式如下圖6所示,表示在A的左孩子B的右子樹(根結點為C,不一定為空)中插入結點(圖中兩個陰影部分之一)而導致不平衡( h 表示子樹的深度)。這種情況調整如下:①將C的右孩子B提升為新的根結點;②將原來的根結點A降為C的右孩子;③各子樹按大小關系連接(BL和AR不變,CL和CR分別調整為B的右子樹和A的左子樹)。
(4)RL型調整:
由於在A的右孩子(R)的左子樹(L)上插入新結點,使原來平衡二叉樹變得不平衡,此時A的平衡因子由-1變為-2。圖7是RL型的最簡單形式。顯然,按照大小關系,結點C應作為新的根結點,其余兩個節點分別作為左右孩子節點才能平衡。
RL型調整的一般形式如下圖8所示,表示在A的右孩子B的左子樹(根結點為C,不一定為空)中插入結點(圖中兩個陰影部分之一)而導致不平衡( h 表示子樹的深度)。這種情況調整如下:①將C的右孩子B提升為新的根結點;②將原來的根結點A降為C的左孩子;③各子樹按大小關系連接(AL和BR不變,CL和CR分別調整為A的右子樹和B的左子樹)。
3、分析
4、代碼
#include <iostream> #include <algorithm> using namespace std; #pragma once //平衡二叉樹結點 template <typename T> struct AvlNode { T data; int height; //結點所在高度 AvlNode<T> *left; AvlNode<T> *right; AvlNode<T>(const T theData) : data(theData), left(NULL), right(NULL), height(0){} }; //AvlTree template <typename T> class AvlTree { public: AvlTree<T>(){} ~AvlTree<T>(){} AvlNode<T> *root; //插入結點 void Insert(AvlNode<T> *&t, T x); //刪除結點 bool Delete(AvlNode<T> *&t, T x); //查找是否存在給定值的結點 bool Contains(AvlNode<T> *t, const T x) const; //中序遍歷 void InorderTraversal(AvlNode<T> *t); //前序遍歷 void PreorderTraversal(AvlNode<T> *t); //最小值結點 AvlNode<T> *FindMin(AvlNode<T> *t) const; //最大值結點 AvlNode<T> *FindMax(AvlNode<T> *t) const; private: //求樹的高度 int GetHeight(AvlNode<T> *t); //單旋轉 左 AvlNode<T> *LL(AvlNode<T> *t); //單旋轉 右 AvlNode<T> *RR(AvlNode<T> *t); //雙旋轉 右左 AvlNode<T> *LR(AvlNode<T> *t); //雙旋轉 左右 AvlNode<T> *RL(AvlNode<T> *t); }; template <typename T> AvlNode<T> * AvlTree<T>::FindMax(AvlNode<T> *t) const { if (t == NULL) return NULL; if (t->right == NULL) return t; return FindMax(t->right); } template <typename T> AvlNode<T> * AvlTree<T>::FindMin(AvlNode<T> *t) const { if (t == NULL) return NULL; if (t->left == NULL) return t; return FindMin(t->left); } template <typename T> int AvlTree<T>::GetHeight(AvlNode<T> *t) { if (t == NULL) return -1; else return t->height; } //單旋轉 //左左插入導致的不平衡 template <typename T> AvlNode<T> * AvlTree<T>::LL(AvlNode<T> *t) { AvlNode<T> *q = t->left; t->left = q->right; q->right = t; t = q; t->height = max(GetHeight(t->left), GetHeight(t->right)) + 1; q->height = max(GetHeight(q->left), GetHeight(q->right)) + 1; return q; } //單旋轉 //右右插入導致的不平衡 template <typename T> AvlNode<T> * AvlTree<T>::RR(AvlNode<T> *t) { AvlNode<T> *q = t->right; t->right = q->left; q->left = t; t = q; t->height = max(GetHeight(t->left), GetHeight(t->right)) + 1; q->height = max(GetHeight(q->left), GetHeight(q->right)) + 1; return q; } //雙旋轉 //插入點位於t的左兒子的右子樹 template <typename T> AvlNode<T> * AvlTree<T>::LR(AvlNode<T> *t) { //雙旋轉可以通過兩次單旋轉實現 //對t的左結點進行RR旋轉,再對根節點進行LL旋轉 RR(t->left); return LL(t); } //雙旋轉 //插入點位於t的右兒子的左子樹 template <typename T> AvlNode<T> * AvlTree<T>::RL(AvlNode<T> *t) { LL(t->right); return RR(t); } template <typename T> void AvlTree<T>::Insert(AvlNode<T> *&t, T x) { if (t == NULL) t = new AvlNode<T>(x); else if (x < t->data) { Insert(t->left, x); //判斷平衡情況 if (GetHeight(t->left) - GetHeight(t->right) > 1) { //分兩種情況 左左或左右 if (x < t->left->data)//左左 t = LL(t); else //左右 t = LR(t); } } else if (x > t->data) { Insert(t->right, x); if (GetHeight(t->right) - GetHeight(t->left) > 1) { if (x > t->right->data) t = RR(t); else t = RL(t); } } else ;//數據重復 t->height = max(GetHeight(t->left), GetHeight(t->right)) + 1; } template <typename T> bool AvlTree<T>::Delete(AvlNode<T> *&t, T x) { //t為空 未找到要刪除的結點 if (t == NULL) return false; //找到了要刪除的結點 else if (t->data == x) { //左右子樹都非空 if (t->left != NULL && t->right != NULL) {//在高度更大的那個子樹上進行刪除操作 //左子樹高度大,刪除左子樹中值最大的結點,將其賦給根結點 if (GetHeight(t->left) > GetHeight(t->right)) { t->data = FindMax(t->left)->data; Delete(t->left, t->data); } else//右子樹高度更大,刪除右子樹中值最小的結點,將其賦給根結點 { t->data = FindMin(t->right)->data; Delete(t->right, t->data); } } else {//左右子樹有一個不為空,直接用需要刪除的結點的子結點替換即可 AvlNode<T> *old = t; t = t->left ? t->left: t->right;//t賦值為不空的子結點 delete old; } } else if (x < t->data)//要刪除的結點在左子樹上 { //遞歸刪除左子樹上的結點 Delete(t->left, x); //判斷是否仍然滿足平衡條件 if (GetHeight(t->right) - GetHeight(t->left) > 1) { if (GetHeight(t->right->left) > GetHeight(t->right->right)) { //RL雙旋轉 t = RL(t); } else {//RR單旋轉 t = RR(t); } } else//滿足平衡條件 調整高度信息 { t->height = max(GetHeight(t->left), GetHeight(t->right)) + 1; } } else//要刪除的結點在右子樹上 { //遞歸刪除右子樹結點 Delete(t->right, x); //判斷平衡情況 if (GetHeight(t->left) - GetHeight(t->right) > 1) { if (GetHeight(t->left->right) > GetHeight(t->left->left)) { //LR雙旋轉 t = LR(t); } else { //LL單旋轉 t = LL(t); } } else//滿足平衡性 調整高度 { t->height = max(GetHeight(t->left), GetHeight(t->right)) + 1; } } return true; } //查找結點 template <typename T> bool AvlTree<T>::Contains(AvlNode<T> *t, const T x) const { if (t == NULL) return false; if (x < t->data) return Contains(t->left, x); else if (x > t->data) return Contains(t->right, x); else return true; } //中序遍歷 template <typename T> void AvlTree<T>::InorderTraversal(AvlNode<T> *t) { if (t) { InorderTraversal(t->left); cout << t->data << ' '; InorderTraversal(t->right); } } //前序遍歷 template <typename T> void AvlTree<T>::PreorderTraversal(AvlNode<T> *t) { if (t) { cout << t->data << ' '; PreorderTraversal(t->left); PreorderTraversal(t->right); } }
紅黑樹
https://www.cnblogs.com/yyxt/p/4983967.html
https://www.cnblogs.com/yyxt/p/4983967.html
多路查找樹(B樹)
等下再補
散列表(哈希表)查找
一、散列表相關概念
散列技術是在記錄的存儲位置和它的關鍵字之間建立一個確定的對應關系f,使得每個關鍵字key對應一個存儲位置f(key)。建立了關鍵字與存儲位置的映射關系,公式如下:
存儲位置 = f(關鍵字)
采用散列技術將記錄存在在一塊連續的存儲空間中,這塊連續存儲空間稱為散列表或哈希表。那么,關鍵字對應的記錄存儲位置稱為散列地址。
散列技術既是一種存儲方法也是一種查找方法。散列技術的記錄之間不存在什么邏輯關系,它只與關鍵字有關,因此,散列主要是面向查找的存儲結構。
二、散列函數的構造方法
2.1 直接定址法
所謂直接定址法就是說,取關鍵字的某個線性函數值為散列地址,即
優點:簡單、均勻,也不會產生沖突。
缺點:需要事先知道關鍵字的分布情況,適合查找表較小且連續的情況。
由於這樣的限制,在現實應用中,此方法雖然簡單,但卻並不常用。
2.2 數字分析法
如果關鍵字時位數較多的數字,比如11位的手機號"130****1234",其中前三位是接入號;中間四位是HLR識別號,表示用戶號的歸屬地;后四為才是真正的用戶號。如下圖所示。
如果現在要存儲某家公司的登記表,若用手機號作為關鍵字,極有可能前7位都是相同的,選擇后四位成為散列地址就是不錯的選擇。若容易出現沖突,對抽取出來 的數字再進行反轉、右環位移等。總的目的就是為了提供一個散列函數,能夠合理地將關鍵字分配到散列表的各個位置。
數字分析法通過適合處理關鍵字位數比較大的情況,如果事先知道關鍵字的分布且關鍵字的若干位分布比較均勻,就可以考慮用這個方法。
2.3 平方取中法
這個方法計算很簡單,假設關鍵字是1234,那么它的平方就是1522756,再抽取中間的3位就是227,用做散列地址。
平方取中法比較適合不知道關鍵字的分布,而位數又不是很大的情況。
2.4 折疊法
折疊法是將關鍵字從左到右分割成位數相等的幾部分(注意最后一部分位數不夠時可以短些),然后將這幾部分疊加求和,並按散列表表長,取后幾位作為散列地址。
比如關鍵字是9876543210,散列表表長為三位,將它分為四組,987|654|321|0,然后將它們疊加求和987 + 654 + 321 + 0 = 1962,再求后3位得到散列地址962。
折疊法事先不需要知道關鍵字的分布,適合關鍵字位數較多的情況。
2.5 除留余數法
此方法為最常用的構造散列函數方法。對於散列表長為m的散列函數公式為:
mod是取模(求余數)的意思。事實上,這方法不僅可以對關鍵字直接取模,也可以再折疊、平方取中后再取模。
很顯然,本方法的關鍵在於選擇合適的p,p如果選不好,就可能會容易產生沖突。
根據前輩們的經驗,若散列表的表長為m,通常p為小於或等於表長(最好接近m)的最小質數或不包含小於20質因子的合數。
2.6 隨機數法
選擇一個隨機數,取關鍵字的隨機函數值為它的散列地址。也就是f(key) = random(key)。這里random是隨機函數。當關鍵字的長度不等時,采用這個方法構造散列函數是比較合適的。
總之,現實中,應該視不同的情況采用不同的散列函數,這里只能給出一些考慮的因素來提供參考:
(1)計算散列地址所需的時間
(2)關鍵字的長度;
(3)散列表的長度;
(4)關鍵字的分布情況;
(5)記錄查找的頻率。
綜合以上等因素,才能決策選擇哪種散列函數更合適。
三、處理散列沖突的方法
在理想的情況下,每一個關鍵字,通過散列函數計算出來的地址都是不一樣的,可現實中,這只是一個理想。市場會碰到兩個關鍵字key1 != key2,但是卻有f(key1) = f(key2),這種現象稱為沖突。出現沖突將會造成查找錯誤,因此可以通過精心設計散列函數讓沖突盡可能的少,但是不能完全避免。
3.1 開放定址法
所謂的開放定址法就是一旦發生了沖突,就去尋找下一個空的散列地址,只要散列表足夠大,空的散列地址總能找到,並將記錄存入。
它的公式為:
比如說,關鍵字集合為{12, 67, 56, 16, 25, 37, 22, 29, 15, 47, 48, 34},表長為12。散列函數f(key) = key mod 12。
當計算前5個數{12, 67, 56, 16, 25}時,都是沒有沖突的散列地址,直接存入,如下表所示。
計算key = 37時,發現f(37) = 1,此時就與25所在的位置沖突。於是應用上面的公式f(37) = (f(37) + 1) mod 12 =2,。於是將37存入下標為2的位置。如下表所示。
接下來22,29,15,47都沒有沖突,正常的存入,如下標所示。
到了48,計算得到f(48) = 0,與12所在的0位置沖突了,不要緊,我們f(48) = (f(48) + 1) mod 12 = 1,此時又與25所在的位置沖突。於是f(48) = (f(48) + 2) mod 12 = 2,還是沖突......一直到f(48) = (f(48) + 6) mod 12 = 6時,才有空位,如下表所示。
把這種解決沖突的開放定址法稱為線性探測法。
考慮深一步,如果發生這樣的情況,當最后一個key = 34,f(key) = 10,與22所在的位置沖突,可是22后面沒有空位置了,反而它的前面有一個空位置,盡管可以不斷地求余后得到結果,但效率很差。因此可以改進di=12, -12, 22, -22.........q2, -q2(q<= m/2),這樣就等於是可以雙向尋找到可能的空位置。對於34來說,取di = -1即可找到空位置了。另外,增加平方運算的目的是為了不讓關鍵字都聚集在某一塊區域。稱這種方法為二次探測法。
還有一種方法,在沖突時,對於位移量di采用隨機函數計算得到,稱之為隨機探測法。
既然是隨機,那么查找的時候不也隨機生成di 嗎?如何取得相同的地址呢?這里的隨機其實是偽隨機數。偽隨機數就是說,如果設置隨機種子相同,則不斷調用隨機函數可以生成不會重復的數列,在查找時,用同樣的隨機種子,它每次得到的數列是想通的,相同的di 當然可以得到相同的散列地址。
總之,開放定址法只要在散列表未填滿時,總是能找到不發生沖突的地址,是常用的解決沖突的方法。
3.2 再散列函數法
對於散列表來說,可以事先准備多個散列函數。
這里RHi 就是不同的散列函數,可以把前面說的除留余數、折疊、平方取中全部用上。每當發生散列地址沖突時,就換一個散列函數計算。
這種方法能夠使得關鍵字不產生聚集,但相應地也增加了計算的時間。
3.3 鏈地址法
將所有關鍵字為同義詞的記錄存儲在一個單鏈表中,稱這種表為同義詞子表,在散列表中只存儲所有同義詞子表前面的指針。對於關鍵字集合{12, 67, 56, 16, 25, 37, 22, 29, 15, 47, 48, 34},用前面同樣的12為余數,進行除留余數法,可以得到下圖結構。
此時,已經不存在什么沖突換地址的問題,無論有多少個沖突,都只是在當前位置給單鏈表增加結點的問題。
鏈地址法對於可能會造成很多沖突的散列函數來說,提供了絕不會出現找不到地址的保證。當然,這也就帶來了查找時需要遍歷單鏈表的性能損耗。
3.4 公共溢出區法
這個方法其實更好理解,你沖突是吧?那重新給你找個地址。為所有沖突的關鍵字建立一個公共的溢出區來存放。
就前面的例子而言,共有三個關鍵字37、48、34與之前的關鍵字位置有沖突,那就將它們存儲到溢出表中。如下圖所示。
在查找時,對給定值通過散列函數計算出散列地址后,先與基本表的相應位置進行比對,如果相等,則查找成功;如果不相等,則到溢出表中進行順序查找。如果相對於基本表而言,有沖突的數據很少的情況下,公共溢出區的結構對查找性能來說還是非常高的。
四、代碼
#include <stdio.h> #include <stdlib.h> #define OK 1 #define ERROR 0 #define SUCCESS 1 #define UNSUCCESS 0 #define HASHSIZE 12 //定義散列表表未數組的長度 #define NULLKEY -32768 typedef struct { int *elem; //數據元素存儲基地址,動態分配數組 int count; //當前數據元素個數 }HashTable; int m = 0; //散列表長,全局變量 //初始化散列表 int InitHashTable(HashTable *h) { int i; m = HASHSIZE; h->elem = (int *)malloc(sizeof(int) * m ); if(h->elem == NULL) { fprintf(stderr, "malloc() error.\n"); return ERROR; } 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; //直到有空位后插入關鍵字 } //散列表查找關鍵字 int SearchHash(HashTable h, int key) { int addr = Hash(key); //求散列地址 while(h.elem[addr] != key) //如果不為空,則沖突 { addr = (addr + 1) % m; //開放地址法的線性探測 if(h.elem[addr] == NULLKEY || addr == Hash(key)) { //如果循環回原點 printf("查找失敗, %d 不在Hash表中.\n", key); return UNSUCCESS; } } printf("查找成功,%d 在Hash表第 %d 個位置.\n", key, addr); return SUCCESS; } int main(int argc, char **argv) { int i = 0; int num = 0; HashTable h; //初始化Hash表 InitHashTable(&h); //未插入數據之前,打印Hash表 printf("未插入數據之前,Hash表中內容為:\n"); for(i = 0; i < HASHSIZE; i++) { printf("%d ", h.elem[i]); } printf("\\n"); //插入數據 printf("現在插入數據,請輸入(A代表結束哦).\n"); while(scanf("%d", &i) == 1 && num < HASHSIZE) { if(i == 'a') { break; } num++; InsertHash(&h,i); if(num > HASHSIZE) { printf("插入數據超過Hash表大小\n"); return ERROR; } } //打印插入數據后Hash表的內容 printf("插入數據后Hash表的內容為:\n"); for(i = 0; i < HASHSIZE; i++) { printf("%d ", h.elem[i]); } printf("\n"); printf("現在進行查詢.\n"); SearchHash(h, 12); SearchHash(h, 100); return 0; }
五、散列表的性能分析
如果沒有沖突,散列查找是所介紹過的查找中效率最高的。因為它的時間復雜度為O(1)。但是,沒有沖突的散列只是一種理想,在實際應用中,沖突是不可避免的。
那散列查找的平均查找長度取決於哪些因素呢?
(1)散列函數是否均勻
散列函數的好壞直接影響着出現沖突的頻繁程度,但是,不同的散列函數對同一組隨機的關鍵字,產生沖突的可能性是相同的(為什么??),因此,可以不考慮它對平均查找長度的影響。
(2)處理沖突的方法
相同的關鍵字、相同的散列函數,但處理沖突的方法不同,會使得平均查找長度不同。如線性探測處理沖突可能會產生堆積,顯然就沒有二次探測好,而鏈地址法處理沖突不會產生任何堆積,因而具有更好的平均查找性能。
(3)散列表的裝填因子
所謂的裝填因子a = 填入表中的記錄個數/散列表長度。a標志着散列表的裝滿的程度。當填入的記錄越多,a就越大,產生沖突的可能性就越大。也就說,散列表的平均查找長度取決於裝填因子,而不是取決於查找集合中的記錄個數。
不管記錄個數n有多大,總可以選擇一個合適的裝填因子以便將平均查找長度限定在一個范圍之內,此時散列表的查找時間復雜度就是O(1)了。為了這個目標,通常將散列表的空間設置的比查找表集合大。
六、散列表的適應范圍
散列技術最適合的求解問題是查找與給定值相等的記錄。對於查找來說,簡化了比較過程,效率會大大提高。
但是,散列技術不具備很多常規數據結構的能力,比如
- 同樣的關鍵字,對應很多記錄的情況,不適合用散列技術;
- 散列表也不適合范圍查找等等。
參考資料:
https://blog.csdn.net/qq_28267025/article/details/78486192(順序表查找)
https://www.cnblogs.com/cuglkb/p/9324050.html(有序表查找)
https://blog.csdn.net/championlee_cs/article/details/46443415(二叉排序樹的C++實現)
https://blog.csdn.net/zxzxzx0119/article/details/80012374(二叉樹,程序)
https://www.cnblogs.com/zhangbaochong/p/5164994.html(AVL樹)
https://blog.csdn.net/qq_24336773/article/details/81712866(AVL樹)
https://www.cnblogs.com/changyaohua/p/4657205.html(散列表查找)