對於數據的基本操做是增刪查改(CRUD)(Create(建立)、Retrieve(檢索)、Update(更新,更改)、Delete(刪除)),咱們能夠注意到對於刪和改兩個基本操做,通常在進行以前都會先進行查找操做。所以查找技術至關重要,也由此產生了專門面向查找技術的各類數據結構。查找技術是多元化的,好比數據庫數據的查找,或者對於電腦文件的快速查找等等。這些查找技術一部分是須要咱們緊緊掌握的,一部分是須要咱們深入理解的。下面開始對部分查找技術進行介紹:web
在查找問題中,一般將數據元素稱爲記錄。
關鍵碼:能夠標識一個記錄的數據項稱爲關鍵碼,關鍵碼的值稱爲鍵值,若關鍵碼能夠惟一標識一個記錄,則稱此關鍵碼爲主關鍵碼,反之稱此關鍵碼爲次關鍵碼。
廣義地講,查找是在具備相同類型的記錄構成的集合中找出知足給定條件的記錄。給定的條件多是多種多樣的,便與討論,咱們把給定條件限制爲「匹配」,即查找的關鍵碼等於給定值的記錄。
靜態查找與動態查找:不涉及插入和刪除的查找稱爲靜態查找,反之爲動態查找,動態查找不成功時,須要將被查找的記錄插入到查找集合中,查找的結果可能會改變查找集合。
查找結構:通常而言,各類數據結構都會涉及到查找操做,有些數據結構專門面向查找操做,稱爲查找結構。
查找算法性能計算:將查找算法進行關鍵碼的比較次數的數學指望值定義爲平均查找長度(average search length),對於查找成功的狀況,計算公式爲:ASL=pi*ci(i從1到n的和),其中n爲問題規模,查找集合中的記錄個數。pi爲查找第i個記錄的機率,ci爲查找第i個記錄須要關鍵碼的比較次數。
下面主要介紹的查找結構爲:
線性表:適用於靜態查找,主要採用順序查找技術,折半查找技術。
樹表:適用於動態查找,主要採用二叉排序樹的查找技術。
散列表:靜態查找和動態查找均適用,主要採用散列技術。算法
線性表對應的順序存儲結構以及鏈式存儲結構決定了線性表的不一樣查找方式。
順序查找,sequential search又稱線性查找,是最基本的查找技術之一,基本思想:從線性表的一端向另外一端逐個將關鍵碼與給定值進行比較。若相等,則查找成功,給出該記錄在表中的位置,若整個表檢測完仍未找到與給定值相等的關鍵碼,則查找失敗,給出失敗信息。
順序表的順序查找:
設置哨兵(待查值)(將它放在查找方向的盡頭處,從另外一端開始查找,減小了每次查找過程當中檢查是否越界的狀況,實驗代表,這種設置在查找大於1000次時,順序查找的平均時間幾乎減少了一半。)
實際上:一切簡化邊界條件而引入的附加節點(或記錄)都可稱爲哨兵,例如單鏈表中的頭結點。
例:數據庫
int SeqSearch(int r[],int n,int k){ r[0]=k; //利用下標0做爲監視哨 i=n; while(r[i]!=k){ //不用判斷下標是否越界 i--; return i; }
單鏈表的順序查找算法:數據結構
int SeqSearch(Node<int>*firsdt,int k){ p=first->next; //工做指針p初始化爲指向第一個元素節點 count=1; while(p!=NULL&&p->data!=k){ p=p->next; //工做指針後移 j++; } if(p->data==k){ return j; } else{ return 0; } }
比較:順序查找與其餘查找技術比較,平均查找長度較大,特別是n較大時,查找效率很低。
折半查找:
要求:線性表中的記錄必須按關鍵碼有序,而且採用順序存儲結構。折半查找技術通常只能用於靜態查找。
例:
折半查找——非遞歸算法:svg
int BinSearch1(int r[ ], int n, int k){ int low=1; high=n,mid; while (low<=high) { mid=(low+high)/2; if (k<r[mid]) high=mid-1; else if (k>r[mid]) low=mid+1; else return mid; } return 0; }
折半查找——遞歸算法:函數
int BinSearch2(int r[ ], int low, int high, int k){ if (low>high) return 0; else { mid=(low+high)/2; if (k<r[mid]) return BinSearch2(r, low, mid-1, k); else if (k>r[mid]) return BinSearch2(r, mid+1, high, k); else return mid; } }
根據折半查找的過程,咱們能夠想到二叉樹的邏輯結構,咱們嘗試用二叉樹的結構來模擬折半查找的過程,由此造成了斷定樹:
樹中的每一個結點對應有序表中的一個記錄,結點的值爲該記錄在表中的位置。一般稱這個描述折半查找過程的二叉樹爲折半查找斷定樹,簡稱斷定樹。
斷定樹的構造方法:
當n=0時,折半查找斷定樹爲空;
當n>0時,折半查找斷定樹的根節點爲mid=(n+1)/2;
根節點的左子樹是與有序表r[1]r[mid-1]相對應的折半查找斷定樹,根節點的右子樹是與r[mid+1]r[n]相對應的折半查找斷定樹。
具體結構以下:
性質:
任意結點的左右子樹中結點個數最多相差1
任意結點的左右子樹的高度最多相差1
任意兩個葉子所處的層次最多相差1性能
查找成功:
在表中查找任一記錄的過程,便是折半查找斷定樹中從根結點到該記錄結點的路徑,和給定值的比較次數等於該記錄結點在樹中的層數。
查找成功時的平均查找長度ASL:
查找不成功:
查找失敗的過程就是走了一條從根結點到外部結點的路徑,和給定值進行的關鍵碼的比較次數等於該路徑上內部結點的個數(失敗狀況下的平均查找長度等於樹的高度)。設計
上述結構查找成功時:
該樹的ASLsucc=(1+22+34+4*4)/11=33/11=3指針
上述結構查找失敗時:
查找不成功時的ASLusucc= ( 34+48) /12=11/3
任意兩棵折半查找斷定樹,若它們的結點個數相同,則它們的結構徹底相同
具備n個結點的折半查找樹的高度爲:
code
線性表查找是靜態的查找,要在線性表上進行動態查找,存在如下的問題:
1、無序順序表上進行動態查找,插入操做簡單,但查找的複雜性高
2、有序順序表上進行動態查找,查找的時間複雜性好,可是插入操做時間複雜性高
3、單鏈表上進行動態查找,插入操做簡單,但查找操做複雜性高
解決辦法:
採用二叉樹這種數據結構,實現動態查找。
二叉排序樹(也稱二叉查找樹)
(1)若它的左子樹不空,則左子樹上全部結點的值均小於根結點的值;
⑵若它的右子樹不空,則右子樹上全部結點的值均大於根結點的值;
⑶ 它的左右子樹也都是二叉排序樹。
中序遍歷二叉排序樹能夠獲得一個按關鍵碼有序的序列。
二叉排序樹的插入算法:
若二叉排序樹爲空樹,則新插入的結點爲新的根結點;
不然,若是插入的值比根節點值大,則在右子樹中進行插入;不然,在左子樹中進行插入。
遞歸實現。
例:
void BiSortTree::InsertBST(BiNode<int> * &root, BiNode<int> *s) { if (root==NULL) root=s; else{ if (s->data<root->data) InsertBST(root->lchild, s); else InsertBST(root->rchild, s); } }
二叉排序樹的刪除:
二叉排序樹的刪除分爲如下三種狀況考慮:
1.被刪除的結點是葉子:操做:將雙親結點中相應指針域的值改成空。
2.被刪除的結點只有左子樹或者只有右子樹:操做:將雙親結點的相應指針域的值指向被刪除結點的左子樹(或右子樹)。
3.被刪除的結點既有左子樹,也有右子樹:
查找結點p的右子樹上的最左下結點s及s雙親結點par;
將結點s數據域替換到被刪結點p的數據域;
若結點p的右孩子無左子樹,則將s的右子樹接到par的右子樹上; 不然,將s的右子樹接到結點par的左子樹上;
刪除結點s;
例:
void BiSortTree::DeleteBST(BiNode<int> *p, BiNode<int> *f ) { if (!p->lchild && !p->rchild) { if(f->child==p) f->lchild= NULL; else f->lchild= NULL; delete p; } else if (!p->rchild) { //p只有左子樹 if(f->child==p) f->lchild=p->lchild; else f->rchild=p->lchild; delete p; } else if (!p->lchild) { //p只有右子樹 if(f->child==p) f->lchild=p->rchild; else f->rchild=p->rchild; delete p; } else { //左右子樹均不空 par=p; s=p->rchild; while (s->lchild!=NULL) //查找最左下結點 { par=s; s=s->lchild; } p->data=s->data; if (par==p) p->rchild=s->rchild; //處理特殊狀況 else par->lchild=s->rchild; //通常狀況 delete s; } //左右子樹均不空的狀況處理完畢 }
二叉排序樹的查找:二叉排序樹的查找性能取決於二叉排序樹的形狀,在O(log2n)和O(n)之間。
在二叉排序樹中查找給定值k的過程是:
⑴ 若root是空樹,則查找失敗;
⑵ 若k=root->data,則查找成功;不然
⑶ 若k<root->data,則在root的左子樹上查找;不然
⑷ 在root的右子樹上查找。
上述過程一直持續到k被找到或者待查找的子樹爲空,若是待查找的子樹爲空,則查找失敗。
二叉排序樹的查找效率在於只需查找二個子樹之一。
例:
BiNode *BiSortTree::SearchBST(BiNode<int> *root, int k) { if (root==NULL) return NULL; else if (root->data==k) return root; else if (k<root->data) return SearchBST(root->lchild, k); else return SearchBST(root->rchild, k); }
二叉排序樹的判斷(判斷一棵樹是不是二叉排序樹):
若是是空樹,返回true;
若是是葉子,返回true;
不然
bst=true
若是root->lchild 是BST
若是root->rchild是BST,
尋找root的最左的分支和root的最右分支並比較大小
若是不知足,bst= false
bst=false
例:
bool BiTree<T>:: Is_BST(BiNode<T> *root){ if(root==NULL) return true; if( root->lchild==NULL && root->rchild==NULL) return true; else { bool l_bst,r_bst,bst=true; l_bst=Is_BST(root->lchild); if(l_bst) { r_bst=Is_BST(root->rchild); if(r_bst) { BiNode <T> *l_child,*l_pre=NULL, *r_child,*r_pre=NULL; l_child=root->lchild; r_child=root->rchild; while(l_child) { l_pre=l_child; l_child=l_child->rchild; } while(r_child) { r_pre=r_child; r_child=r_child->lchild; } if(l_pre&&root->data<l_pre->data || r_pre&&root->data>r_pre->data) bst=false; } else bst=false; } else bst=false; return bst; } }
基於遍歷的思想判斷
在中序遍歷的過程當中查看中序遍歷中相鄰的兩個節點之間是否正序,是則繼續遍歷,不然,爲非二叉排序樹。
例:
template<class T> void BiTree<T>::In_BST(BiNode<T> *root,int &key,bool &bst){ if(root&&bst) { In_BST(root->lchild,key,bst); if(key<=root->data ) { key=root->data; } else bst=false; In_BST(root->rchild,key,bst); } } template<class T> bool BiTree<T>:: Is_BST_2(BiNode<T> *root) { int data; BiNode<T> * pre,*t_root; t_root=root; while(root!=NULL) { pre=root; root=root->lchild; } data=pre->data; //找着中序遍歷的第一個節點; bool bst=true; In_BST(t_root,data,bst); if(bst) return true; else return false; }
平衡二叉樹(AVL樹)
或者是一棵空的二叉排序樹,或者是具備下列性質的二叉排序樹:
⑴ 根結點的左子樹和右子樹的深度最多相差1;
⑵ 根結點的左子樹和右子樹也都是平衡二叉樹。
平衡因子:結點的平衡因子是該結點的左子樹的深度與右子樹的深度之差。 在平衡樹中,結點的平衡因子能夠是1,0,-1。(由平衡二叉樹的定義性質決定)
最小不平衡子樹:在平衡二叉樹的構造過程當中,以距離插入結點最近的、且平衡因子的絕對值大於1的結點爲根的子樹。 這時咱們就要對最小不平衡子樹做出相應調整使得該樹仍然爲平衡二叉樹。所以在構造二叉排序樹的過程當中,每插入一個結點時,首先檢查是否因插入而破壞了樹的平衡性。
設結點A爲最小不平衡子樹的根結點,對該子樹進行平衡調整概括起來有如下四種狀況:
1.LL型:
B=A->lchild; A->lchild=B->rchild; B->rchild=A; A->bf=0; B->bf=0; if (FA==NULL) root=B; else if (A==FA->lchild) FA->lchild=B; else FA->rchild=B;
B=A->rchild; A->rchild=B->lchild; B->lchild=A; A->bf=0; B->bf=0; if (FA==NULL) root=B; else if (A==FA->lchild) FA->lchild=B; else FA->rchild=B;
3.LR型:
4.RL型
概念:
前面接觸到的查找技術(順序查找,折半查找,二叉排序樹等)都是經過一系列的給定值與關鍵碼的比較,查找效率依賴於查找過程當中進行的給定值與關鍵碼的比較次數。爲了提升效率,能夠採用不比較的方式,在存儲位置與關鍵碼之間創建一個肯定的對應關係。這樣,不通過比較,一次讀取就能獲得所查元素的查找方法。
散列表:採用散列技術將記錄存儲在一塊連續的存儲空間中,這塊連續的存儲空間稱爲散列表。
散列函數:將關鍵碼映射爲散列表中適當存儲位置的函數。
散列地址:由散列函數所得的存儲位置址 。
關係以下:
由上圖得:散列既是一種查找技術,也是一種存儲技術。
是經過記錄的關鍵碼定位該記錄,沒有完整地表達記錄之間的邏輯關係,因此,散列主要是面向查找的存儲結構。
限制:散列技術通常不適用於容許多個記錄有一樣關鍵碼的狀況。散列方法也不適用於範圍查找,不能查找最大值、最小值,也不可能找到在某一範圍內的記錄。
散列技術的關鍵問題:散列函數的設計,衝突的處理。
衝突:對於兩個不一樣關鍵碼ki≠kj,有H(ki)=H(kj),即兩個不一樣的記錄須要存放在同一個存儲位置,ki和kj相對於H稱作同義詞。
設計散列函數通常應遵循如下原則:
⑴ 計算簡單。散列函數不該該有很大的計算量,不然會下降查找效率。
⑵ 函數值即散列地址分佈均勻。函數值要儘可能均勻散佈在地址空間,這樣才能保證存儲空間的有效利用並減小衝突。
散列函數的構造:
直接定址法:H(key) = a key + b (a,b爲常數)使用條件:事先知道關鍵碼,關鍵碼集合不是很大且連續性較好。
除留餘數法:H(key)=key mod p 除留餘數法是一種最簡單、也是最經常使用的構造散列函數的方法,而且不要求事先知道關鍵碼的分佈。 通常狀況下,選p爲小於或等於表長(最好接近表長)的最小素數。
數字分析法:根據關鍵碼在各個位上的分佈狀況,選取分佈比較均勻的若干位組成散列地址。適用狀況: 事先知道關鍵碼的分佈,關鍵碼的分佈均勻:
例:關鍵碼爲8位十進制數,散列地址爲2位十進制數
平方取中法:對關鍵碼平方後,按散列表大小,取中間的若干位做爲散列地址(平方後截取)。 適用狀況:事先不知道關鍵碼的分佈且關鍵碼的位數不是很大。
摺疊法(分段疊加法):將關鍵碼從左到右分割成位數相等的幾部分,將這幾部分疊加求和,取後幾位做爲散列地址。 適用狀況:關鍵碼位數不少,事先不知道關鍵碼的分佈。
例:設關鍵碼爲2 5 3 4 6 3 5 8 7 0 5,散列地址爲三位。
衝突處理方法:堆積:在處理衝突的過程當中出現的非同義詞之間對同一個散列地址爭奪的現象。
1、閉散列方法( closed hashing,也稱爲開地址方法,open addressing ,開放定址法)。由關鍵碼獲得的散列地址一旦產生了衝突,就去尋找下一個空的散列地址,並將記錄存入。
方法:
(1)線性探測法:
當發生衝突時,從衝突位置的下一個位置起,依次尋找空的散列地址。
對於鍵值key,設H(key)=d,閉散列表的長度爲m,則發生衝突時,尋找下一個散列地址的公式爲:
Hi=(H(key)+di) % m (di=1,2,…,m-1)
線性探測法構造的散列表的查找算法:
int HashSearch1(int ht[ ], int m, int k) { j=H(k); if (ht[j]==k) return j; //沒有發生衝突,比較一次查找成功 i=(j+1) % m; while (ht[i]!=Empty && i!=j) { if (ht[i]==k) return i; //發生衝突,比較若干次查找成功 i=(i+1) % m; //向後探測一個位置 } if (i==j) throw "溢出"; else ht[i]=k; //查找不成功時插入 }
(2)二次探測法:
當發生衝突時,尋找下一個散列地址的公式爲:
Hi=(H(key)+di)% m
(di=12,-12,22,-22,…,q2,-q2且q≤m/2)
(3)隨機探測法:
當發生衝突時,下一個散列地址的位移量是一個隨機數列,即尋找下一個散列地址的公式爲:
Hi=(H(key)+di)% m
(di是一個隨機數列,i=1,2,……,m-1)
(4)再hash法
2、開散列方法( open hashing,也稱爲拉鍊法,separate chaining ,鏈地址法):
基本思想:將全部散列地址相同的記錄,即全部同義詞的記錄存儲在一個單鏈表中(稱爲同義詞子表),在散列表中存儲的是全部同義詞子表的頭指針。
設n個記錄存儲在長度爲m的散列表中,則同義詞子表的平均長度爲n / m。
例:
在拉鍊法構造的散列表查找算法:
Node<int> *HashSearch2(Node<int> *ht[ ], int m, int k) { j=H(k); p=ht[j]; while (p && p->data!=k) p=p->next; if (p->data= =k) return p; else { q=new Node<int>; q->data=k; q->next= ht[j]; ht[j]=q; } }
開散列表與閉散列表的比較:
3、創建公共溢出區:散列表包含基本表和溢出表兩部分(一般溢出表和基本表的大小相同),將發生衝突的記錄存儲在溢出表中。
查找時,對給定值經過散列函數計算散列地址,先與基本表的相應單元進行比較,若相等,則查找成功;不然,再到溢出表中進行順序查找。
散列查找的性能分析:
因爲衝突的存在,產生衝突後的查找仍然是給定值與關鍵碼進行比較的過程。
在查找過程當中,關鍵碼的比較次數取決於產生衝突的機率。而影響衝突產生的因素有:
(1)散列函數是否均勻
(2)處理衝突的方法
(3)散列表的裝載因子
α=表中填入的記錄數/表的長度
幾種不一樣處理衝突方法的平均查找長度: