文章目錄
第八章 查找
定義
搜索引擎工作:
查找表:是由同一類型的數據元素構成的集合。
關鍵字:是數據元素中某個數據項的值。
主關鍵字:若此關鍵字可以唯一地標識一個記錄,則稱此關鍵字爲主關鍵字(Primary Key)。
次關鍵字:那些可以識別多個數據元素的關鍵字,稱之爲次關鍵詞(Secondary Key)。
查找:根據給定的某個值,在查找表中確定一個關鍵字等於給定值的數據元素。
靜態查找表:只作查找操作的查找表,主要操作有1. 查詢某個“特定的”數據元素是否在表中;2. 檢索某個“特定的”數據元素和各種屬性。
動態查找表:在查找過程中同時插入查找表中不存在的數據元素,或者刪除已經存在的元素。
順序表查找
思路:從頭到尾遍歷比較。
實現代碼:
int Sequential_Search(int *a, int n, int key)
{
int i;
for (i=1; i<=n; i++)
{
if (a[i] == key)
return i;
}
return 0;
}
優化順序查找:
設置一個哨兵,可以不需要 i 每次都和 n 比較。
做一個長度爲n+1的數組,把0位置的值設爲key,倒着查找每次下標減一,一定會出現值爲key的時候,若下標爲0表示沒找到,否則表示找到。
int Sequential_Search2(int *a, int n, int key)
{
int i;
a[0] = key;
i = n;
while (a[i] != key)
{
i--;
}
return i;
}
時間複雜度:
最好的情況爲。
最壞的情況爲。
關鍵字在任一位置的概率是相同的,所以平均查找次數爲,最終時間複雜度爲。
JAVA實現順序查找
public class OrderFine {
public static void main(String[] args) {
int[] a = {2,3,4,5,1,6};
int b = 4;
Find1(a, b);
// 第一個位置爲空,寫爲0
int[] c = {0, 2,3,4,5,1,6};
Find2(c, b);
}
private static void Find2(int[] a, int b) {
a[0] = b;
int len = a.length;
while (a[len-1] != b){
len--;
}
if (len==1){
System.out.println("沒找到");
} else {
System.out.println(len-2);
}
}
private static void Find1(int[] a, int b) {
for (int i = 0; i < a.length; i++) {
if (a[i] == b){
System.out.println(i);
break;
}
}
}
}
有序表查找
折半查找
思路:前提是線性表中的關鍵詞有序,線性表示順序結構。取中間記錄作爲比較對象,若給定值小於中間值,就在左邊區間找;若大於中間值,就在右邊區間找。
代碼實現:
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])
high = mid - 1;
else if (key > a[mid])
low = mid + 1;
else
return mid; // 相等表示找到
}
return 0;
}
插值查找
思路:優化二分查找法,根據key在數值域中大小比例,來確定在哪找。
推導:
核心代碼:
mid = low + (high-low)*(key-a[low]) / (a[high]-a[low]);
斐波那契查找(沒理解)
找數組的長度在F數組中的位置。
根據F中的數字來擴充a數組,後面的值用a數組中的最大值填充
mid的值是由F決定的,mid=low+F[k-1] - 1
比較後修改high,low,k// 爲什麼要這樣改k呢
如果小了,k-1,大了k-2.high並不會影響mid的選擇,low纔會
k在逐漸變小,得到的F值也在變小,F值是a的下標,如果key比a大,那麼low變大了,F也會變得很大,此時k-2的話,得到的F值仍然是大的,
沒看懂啊,後面再看吧
int Fibonacci_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++)
a[i] = a[n];
while (n > F[k]-1) // 看看長度位於F數組的什麼位置
k++;
for (i=n; i<F[k]-1; i++) // a數組的長度順着上面F的取值,要擴充
a[i] = a[n];
while (low <= high)
{
mid = low + F[k-1] - 1; // 計算當前分割下標
if (key < a[mid])
{
high = mid-1;
k = k-1;
}
else if (key > a[mid])
{
low = mid+1;
k = k-2;
}
else
{
if (mid <=n )
return mid;
else
return n;
}
}
return 0;
}
JAVA實現有序表查找
折半:
public class BinarySearch {
public static void main(String[] args) {
int[] a = {1, 2, 3, 4, 5, 6, 7, 8};
int key = 3;
BinarySc(a, key);
}
private static void BinarySc(int[] a, int key) {
int high = a.length-1;
int low = 0;
int min = 0;
while (low <= high){
min = (low + high) / 2;
if (a[min] > key){
high = min - 1;
} else if (a[min] < key){
low = min + 1;
} else {
System.out.println(min);
break;
}
}
}
}
線性索引查找
稠密索引
稠密索引:數據集中每個記錄都有一個索引,索引項一定按照關鍵碼有序排列。
分塊索引
分塊有序:將數據集分塊,按塊給索引,這些塊要滿足下面兩個條件,這樣的序列叫做分塊有序:
- 塊內無序:每個塊內的元素不需要有序
- 塊間有序:比如第二塊的記錄的關鍵字均大於第一塊中所有記錄的關鍵字。
分塊索引表結構:
- 最大關鍵碼,存儲每一個塊中的最大關鍵字,好處是可使下一塊的最小關鍵字也能比上一塊最大的關鍵字大。
- 存儲了塊中的記錄個數,以便循環時使用。
- 用於指向首數據元素的指針,便於開始對這一塊中記錄進行遍歷。
分塊索引表查找步驟:
- 先用簡單的算法找到位於哪個塊
- 然後利用塊的指針, 在塊中順序搜索即可
時間複雜度分析:
共有n個記錄,設有m塊,每塊t條記錄,所以塊的查找假設爲次,
塊中的查詢設爲次,所以總查找爲:
最好的情況是m與t相等,所以次數爲,時間複雜度爲,比折半查找的差不少。
倒排索引
索引項的通用結構:
- 次關鍵碼,例如上面的英文單詞
- 記錄號表,例如上面的文章編號
倒排索引:就是和上面的分塊索引相反,左邊放元素,右邊放在哪個塊。記錄號表存儲具有相同次關鍵碼的所有記錄的記錄號,這樣的索引方法就是倒排索引。
因爲生活中有時需要根據屬性來查找記錄,例如搜索引擎。
二叉排序樹
定義:二叉排序樹(Binary Sort Tree),又稱爲二叉查找樹,它或者是一顆空樹,或者是有下列性質的二叉樹:
- 若左子樹不爲空,則左子樹上所有的結點都小於根結點
- 若右子樹不爲空,則右子樹上所有的結點都大於根結點
- 左右子樹也分別爲二叉排序樹
- 使用中序遍歷可得從小到大的序列
作用:並不是爲了排序,而是爲了提高查找和插入刪除關鍵字的速度。
二叉樹結構:
typedef struct BiTNode // 結點結構
{
int data;
struct BitNode *lchild, *rchild;
} BiTNode, *BitTree;
二叉排序樹的查找
思路:f用來指向雙親,p用來保存結果,找到了就爲此結點,到最後一直沒找到就返回離該點最接近的一個結點。
代碼實現:
Status SearchBST(BiTree T, int key, BiTree f, BiTree *p)
{
if (!T) // 大小比較完的最後,到了null就表示找不到了
{
*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); // 在右子樹繼續找
}
二叉排序樹插入操作
思路:先檢查樹裏有沒有和插入點重複的,有就不插,沒有就找到最接近插入點的結點,根據該結點和插入點的大小關係來判斷爲左孩子還是右孩子。
代碼實現:
Status InsertBST(BiTree *T, int key)
{
BiTree p, s;
if (!SearchBST(*T, key, NULL, &p)) // 沒找到就添加
{
// 先把結點建好
s = (BiTree) malloc(sizeof(BiTNode));
s->data = key;
s->lchild = s->rchild = NULL;
// 添加環節
if (!p) // 如果是個空樹,就把插入的點當成根結點
*T = s;
else if (key < p-data)
p->lchild = s; // 插入s爲左孩子
else
p->rchild = s;
return TRUE;
}
else
return FALSE; // 已經有了相同的關鍵點,不再插入
}
二叉排序樹刪除操作
思路: 找刪除點的中序前驅結點來替換被刪除點,前驅和該刪除點在排序上是相鄰,所以是最適合替換的,同理也可用後驅替換。所以應有三個元素,一是被刪除點,二是前驅點,三是前驅的父結點,前驅的父結點用來把斷的接上。
查找代碼:
刪除前要找到結點,找到後針對結點刪除
Status DeleteBST(BiTree *T, int key)
{
if (!*T) // 不存在的話
return FALSE;
else
{
if (key == (*T)->data) // 找到關鍵字等於key的數據元素
// 此處和查找不同
return Delete(T);
else if (key < (*T)->data)
return DeleteBST(&(*T)->lchild, key);
else
return DeleteBST(&(*T)->rchild, key);
}
}
刪除代碼:
Status Delete(BiTree *p)
{
BiTree q, s;
if ((*p)->rchild == NULL) // 右子樹空則只需要重接左子樹
{
q = *p;
*p = (*p)->lchild;
free(q);
}
else if ((*p)->lchild == NULL) // 同理
{
q = *p;
*p = (*p)->rchild;
free(q);
}
else // 左右子樹均不爲空
{
q = *p;
s = (*p)->lchild;
// 找p的左孩子的右孩子的盡頭,s
while (s->rchild) // s指向被刪除結點的前驅,q指向s的父結點
{
q = s;
s = s->rchild;
}
(*p)->data = s->data; // 將前驅的值賦給被刪除結點位置
// 當q的左孩子擁有右孩子的時候
if (q != *p)
q->rchild = s->lchild; // 重接q的右子樹
// 當q的左孩子沒有右孩子的時候,q=p,沒動
else
// 刪除點的左孩子接替刪除點的位置
// 正因沒有右孩子,所以不會有影響
q->lchild = s->lchild;
free(s);
}
return TRUE;
}
刪除操作圖示:
綠色的線表示結點的變動,藍色的數字表示中序遍歷順序,黃色表示結點
二叉排序樹總結
二叉樹雖然插入刪除比順序表簡單,但也存在問題,樹的結構是很影響速度的。
同樣的數據元素,不同的排列順序,會有不同的樹的結構:
查找結點99,左邊只需比較兩次,而右邊需要比較10次。
所以希望二叉排序樹是比較平衡的,深度與完全二叉樹相同,均爲,所以茶中的複雜度也爲。
JAVA實現二叉排序樹查找、插入、刪除
因爲java中,沒有Tree實例就不能操作,沒有指針,所以定義了一個變量,來標記該結點是否存在。
結點類:
public class Tree {
private int data;
// 因爲java中,沒有Tree實例就不能操作,沒有指針
// 所以再定義一個變量,來標記該結點是否存在
public boolean exist=false;
public Tree lchild, rchild;
public int getData() {
return data;
}
public void setData(int data) {
exist = true;
this.data = data;
}
public Tree() {
lchild = rchild = null;
}
public Tree(int data) {
exist = true;
this.data = data;
lchild = rchild = null;
}
public void equal(Tree t){
if (t != null){
this.exist = true;
this.data = t.getData();
this.lchild = t.lchild;
this.rchild = t.rchild;
} else {
this.exist = false;
}
}
}
測試類:
public class BaseBT {
public static void main(String[] args) {
int[] a = {62, 58, 88, 47, 73, 99, 35, 52, 93, 37};
Tree T = new Tree();
// 創建二叉排序樹,就是一直插入的過程
for (int i = 0; i < a.length; i++) {
SearchOrInsert(T, a[i], null, false);
}
System.out.println("中序遍歷--------");
// 中序遍歷即可得到升序
TraverseTree(T);
// 刪除結點
int key = 52;
System.out.println("刪除的值爲:" + key);
DeleteNode(T, key);
System.out.println("中序遍歷--------");
TraverseTree(T);
}
private static void DeleteNode(Tree t, int key) {
// 首先要找到位置
if (t.exist == false){
System.out.println("無此元素");
} else {
if (key == t.getData()){
Delete(t);
} else if (key < t.getData()){
DeleteNode(t.lchild, key);
} else {
DeleteNode(t.rchild, key);
}
}
}
private static void Delete(Tree t) {
// 如果左右孩子存在空,是最簡單的
// 如果左孩子爲空,那麼可能就是隻有右孩子或都沒有
if (t.lchild == null){
t.equal(t.rchild);
} else if (t.rchild == null){
t.equal(t.lchild);
} else {
// 找前驅
Tree q = t;
// 先找一個左孩子
Tree s = q.lchild;
// 再一直找左孩子的右孩子
while (s.rchild!=null){
q = s;
s = s.rchild;
}
// 此時可以確定t的新值
t.setData(s.getData());
// 此時要看s是否有右孩子,沒有就直接接上
if (q == t){
// 如果左孩子沒有右孩子,那麼t的值就是左孩子,所以去掉左孩子
t.lchild = s.lchild;
} else {
q.rchild.equal(s.lchild);
}
}
}
private static void TraverseTree(Tree t) {
if (t!=null && t.exist){
TraverseTree(t.lchild);
System.out.println(t.getData());
TraverseTree(t.rchild);
}
}
// 查詢插入一體化
private static boolean SearchOrInsert(Tree t, int i, Tree f, boolean search){
// 考慮根結點爲空的情況
// tree不爲空但exits爲false,只有這一種情況
if (t!=null && t.exist==false){
System.out.println("插入了:" + i);
t.setData(i);
return false;
}
// t是樹的根結點,i是被查找元素,f是當前結點的父結點
if (t == null){
if (search){
System.out.println("沒找到");
} else {
System.out.println("插入了:" + i);
if (i > f.getData()){
f.rchild = new Tree(i);
} else {
f.lchild = new Tree(i);
}
}
return false;
}
// 找到了
if (t.getData() == i){
System.out.println("找到了");
return true;
}
// 往右子樹走
if (t.getData() < i){
return SearchOrInsert(t.rchild, i, t, search);
} else {
return SearchOrInsert(t.lchild, i, t, search);
}
}
}
平衡二叉樹AVL
定義:平衡二叉樹是一種二叉排序樹,其中每一個結點的左子樹和右子樹的高度差至多等於1。
平衡因子BF:將二叉樹上結點的左子樹深度減去右子樹深度的值稱爲平衡因子BF。
例子:
最小不平衡子樹:距離插入結點最近的,且平衡因子的絕對值大於1的結點爲根的子樹,稱爲最小不平衡子樹。
案例:
插入了37,58的高度變成了2,BF也變成了2。
平衡二叉樹實現原理
思想:在構建二叉排序樹的過程中,每當插入一個結點時,先檢查是否因插入而破壞了樹的平衡性,若是,則找出最小不平衡子樹。再保持二叉排序樹特性的前提下,調整最小不平衡子樹中各結點之間的鏈接關係,進行相應的旋轉,使之稱爲新的平衡子樹。
案例:數組爲{3,2,1,4,5,6,7,10,9,8},最終結果爲下圖,步驟就不上了,太多了。
方法:
- 出現不平衡問題的時候要立即修正
- 如果最小不平衡樹的根結點爲負數,該最小不平衡樹就左旋,正數就右旋
- 如果最小不平衡樹的根結點和孩子結點的BF符號不一樣,就得調整到符號一樣,調整的方法可能是改變順序(11和12),也可能是旋轉(14和15)
平衡二叉樹實現算法
改進結點,添加BF因子:
typedef struct BiTNode // 結點結構
{
int data;
int bf;
struct BiTNode *lchild, *rchild;
} BiTNode, *BiTree;
旋轉可行的原因:
二叉排序樹的構建和元素的輸入順序有很大關係,而下面的旋轉操作並不像影響排序的正確性,所以旋轉相當於把輸入的順序重新排了,數還是那些數,中序遍歷起來也還是升序的,所以沒問題。
右旋:
- 讓原本根結點的左孩子作新的根結點,原本根結點作爲新根結點的右孩子,重點是,旋轉後的中序遍歷順序不能變。
- BF在別的地方改變,所以不需要在這裏考慮BF
// 對以p爲根的二叉排序樹作右旋
// 之後p指向新的結點
void R_Rotate(BiTree *p)
{
BiTree L;
L = (*p)->lchild;
// 可能存在,也可能不存在
// 如果存在的話,爲了保證正確的的大小順序,具體看下圖
(*p)->lchild = L->rchild;
// 重點就在這,讓p的左孩子成爲新的根結點
L->rchild = (*p);
*p = L;
}
右旋中的排序理解:
紅色爲BF,藍色爲中序順序,注意,此處的BF只是一個參考。
右旋案例:
左旋:
void L_Rotate(BiTree *p)
{
BiTree R;
R = (*p)->rchlid;
(*p)->rchild = R->lchild;
R->lchild = (*p);
*p = R;
}
左平衡旋轉處理:
思路:這裏已經知道是要處理左平衡,所以知道T的BF大於0,直接從根結點的左孩子下手,先判斷左孩子的BF,如果是同號,那麼做簡單的右旋並修改各個結點的BF即可;如果是異號,則以左孩子爲根結點左旋,變爲正BF,再對根結點右旋,同時修改各個結點的BF,這裏的BF修改還跟插在了哪顆子樹相關。
下圖算是比較清晰例子:
#define LH +1 // 左高
#define EH 0 // 等高
#define RH -1 // 右高
// 對T所指結點爲根的二叉樹作左平衡旋轉處理
// 結束時T指向新的根結點
void LeftBalance(BiTree *T)
{
BiTree L, Lr;
L = (*T)->lchild; // 處理左平衡,所以直接左子樹
// 判斷同號還是異號
switch(L->bf)
{
case LH: // 同號,新點插在了T的左孩子的左子樹上,做單右旋處理
(*T)->bf = L->bf = EH;
R_Rotate(T);
break;
case RH: // 異號,插在了左孩子的右子樹上,做雙旋處理
Lr = L->rchild; // 左孩子的右子樹根
// 判斷是在右子樹的何處,藉此修改各結點bf
// 不過=0沒看懂
switch(Lr->bf)
{
case LH:
(*T)->bf = RH;
L->bf = EH;
break;
case EH:
(*T)->bf = L->bf = EH;
break;
case RH:
(*T)->bf = EH;
L->bf = LH;
break;
}
Lr->bf = EH;
L_Rotate(&(*T)->lchild); // 對T的左子樹做左旋
R_Rotate(T); // 對T做右旋
}
}
左平衡的三種case圖例:
對應異號部分的三種情況,藍色爲中序順序,紅色爲BF,N爲新插入的點,在EH情況下,Lr就是N。
- EH:
- LH:
- RH:
左平衡規律總結:
- Lr會成爲新的根結點,且bf爲0。
- 因爲是左平衡且異號,所以變化很固定,先根結點的左孩子左旋,再根結點右旋。
- T和L的bf和孩子取決於,N是Lr的左孩子還是又孩子。如果是左孩子,那麼會小於根結點Lr,所以就會分配給L,所以此時L的bf=0,T的bf=-1。
- 如果N是Lr的右孩子,那麼會大於根結點Lr,就會分配給T,所以此時L的bf=1,T的bf=0。
主函數:
思路:
- 先說插入:這是一個插入函數,每次插入調用一次。想插入就得先找到位置,所以該函數用了遞歸來尋找位置,使用
InsertAVL(&(*T)->lchild...
來遞歸查找,如果輸入的參數T變成了Null,說明找到了位置,即可在該指針處創建結點。 - 再說平衡:由
if (e<(*T)->data)
這個判斷可知道,插入後緊接着就會檢查新插入結點的父結點的BF,並根據BF的值來判斷是否需要對父結點進行平衡並該BF操作,不需要的話就把父結點的BF改了,畢竟插入了,所以一定會變。如果原來的父結點的BF爲0的話,此時高度就會變,所以taller會變成TRUE,此時回到遞歸的上一次,即父結點爲T的時候,此時因爲taller變了,所以還要再判斷是否平衡。因此只要高度變了,就會從下往上根據判斷條件平衡。從下往上的話即不會漏掉,也可以在第一時間平衡。 - 這裏面最近T是被插入的結點的父節點,然後T會一層一層再往上移動,然後層層改變BF。
- 做了左、右平衡後,
taller=False
,此時就不需要再網上判斷了,所以說每插入一次,最多做一次平衡就行了
// 若不存在和e相同的,則插入並返回1,否則返回0
// 若插入後使二叉排序樹失去平衡,則作平衡旋轉處理
// taller反應是否長高
Status InsertAVL(BiTree *T, int e, Status *taller)
{
if (!*T)
{
// 插入新結點,樹長高
*T = (BiTree) malloc(sizeof(BiTNode));
(*T)->data = e;
(*T)->lchild = (*T)->rchild = NULL:
(*T)->bf = EH;
*taller = TRUE;
}
else
{
if (e == (*T)->data)
{ // 已有,不再插入
*taller = FALSE;
return FALSE;
}
if (e < (*T)->data)
{ // 在T的左子樹繼續搜索
if (!InsertAVL(&(*T)->lchild, e, taller))
// 插入失敗
return FALSE;
// 長高了,就得修改BF並考慮平衡問題
if (taller)
{
switch((*T)->bf)
{
case LH: // 原來爲左高,左邊再加一個就不平衡了
LeftBalance(T);
*taller = FALSE;
break;
case EH: // 原來一樣高
(*T)->bf = LH;
*taller = TRUE;
break;
case RH:
// 爲什麼這個也有False?
// 左右變平衡,說明是在短的一邊加的,所以高度不變
(*T)->bf = EH;
*taller = FALSE;
break;
}
}
}
else
{
if (!InsertAVL(&(*T)->rchild, e, taller))
return FALSE;
if (taller)
{
switch((*T)->bf)
{
case LH:
(*T)->bf = EH;
*taller = FALSE;
break;
case EH:
(*T)->bf = RH;
*taller = TRUE;
break;
case RH:
LeftBalance(T);
*taller = FALSE;
break;
}
}
}
}
return TRUE;
}
生成平衡二叉樹:
int i;
int a[10] = {...};
BiTree T = NULL;
Status taller;
for (i=0; i<10; i++)
{
InsertAVL(&T, a[i], &taller);
}
時間複雜度:
查找、刪除、插入都是。
JAVA實現平衡二叉樹相關
結點類:
public class BitNode {
public int BF;
public BitNode lchild, rchild;
private int data;
public boolean exist=false;
public int getData() {
return data;
}
public void setData(int data) {
exist = true;
this.data = data;
}
public BitNode() {
lchild = rchild = null;
}
public BitNode(int data) {
exist = true;
this.data = data;
lchild = rchild = null;
}
public void equal(BitNode bitNode){
if (bitNode!=null)
{
exist = bitNode.exist;
data = bitNode.data;
BF = bitNode.BF;
lchild = bitNode.lchild;
rchild = bitNode.rchild;
} else {
exist = false;
}
}
}
方法類:
public class AVLutils {
public static void L_Rotate(BitNode T) {
// T的分身
BitNode L = new BitNode();
L.equal(T);
// T的右孩子上位新跟結點
T.equal(T.rchild);
L.rchild = T.lchild;
T.lchild = L;
}
public static void R_Rotate(BitNode T) {
// T的分身
BitNode L = new BitNode();
L.equal(T);
// T的右孩子上位新跟結點
T.equal(T.lchild);
L.lchild = T.rchild;
T.rchild = L;
}
public static void LeftBalance(BitNode T){
// 做平衡,T.bf=1,是在變化之前傳進函數的
BitNode L = T.lchild;
// 檢查同號或異號
switch (L.BF){
// 同號的情況
case 1:
// 右轉T
R_Rotate(T);
T.BF = L.BF = 0;
break;
// 異號的情況,這裏要考慮Lr的符號
case -1:
BitNode Lr = L.rchild;
switch (Lr.BF){
// 插在了Lr的右邊,此時新結點跟T走
case -1:
T.BF = 0;
L.BF = 1;
break;
// 插在了Lr的左邊,此時新結點跟L走
case 1:
T.BF = -1;
L.BF = 0;
break;
case 0:
T.BF = L.BF = 0;
break;
}
// 根據規律可得以下固定內容
Lr.BF = 0;
L_Rotate(L);
R_Rotate(T);
}
}
public static void RightBalance(BitNode T){
BitNode R = T.rchild;
switch (R.BF){
// 此時同號爲負
case -1:
L_Rotate(T);
T.BF = R.BF = 0;
case 1:
BitNode Rl = R.lchild;
switch (Rl.BF){
case 0:
T.BF = R.BF = 0;
// 插在了左邊,跟着T
case 1:
T.BF = 0;
R.BF = -1;
case -1:
T.BF = 1;
R.BF = 0;
}
Rl.BF = 0;
R_Rotate(R);
L_Rotate(T);
}
}
// 通過比較來找位置
public static boolean InsertAVL(BitNode T, BitNode f, int e, Status sta){
// 不能存在說明找到要插入的位置了,假設只要插入了就變高
// 其實就是個檢查機制,有插入就檢查
if (T==null){
// 父結點不爲空
if (e > f.getData()){
f.rchild = new BitNode(e);
} else {
f.lchild = new BitNode(e);
}
System.out.println("插入結點:" + e);
sta.taller = true;
return true;
} else {
// f表示父結點,f和T都爲空說明是根結點
if (f==null && T.exist==false){
T.setData(e);
System.out.println("插入結點:" + e);
return true;
}
if (e == T.getData()){
System.out.println("重複了:" + e);
sta.taller = false;
return false;
} else if (e > T.getData()){
// 往右子樹找
// 如果插入失敗,則跳過
if (!InsertAVL(T.rchild, T, e, sta)){
return false;
}
// 插入成功後,檢查高度
if (sta.taller){
// 現在是插到了右邊
switch (T.BF){
case 0:
// 插入打破了平衡,說明最高高度變了
T.BF = -1;
sta.taller = true;
break;
case 1:
// 左右變平衡,說明是在短的一邊加的,所以最高高度不變
T.BF = 0;
sta.taller = false;
break;
case -1:
RightBalance(T);
sta.taller = false;
break;
}
}
} else {
// 往左子樹找
if (!InsertAVL(T.lchild, T, e, sta)){
return false;
}
if (sta.taller){
switch (T.BF){
case 0:
// 插入打破了平衡,說明最高高度變了
sta.taller = true;
T.BF = 1;
break;
case -1:
// 左右變平衡,說明是在短的一邊加的,所以最高高度不變
sta.taller = false;
T.BF = 0;
break;
case 1:
LeftBalance(T);
sta.taller = false;
break;
}
}
}
}
// 能走到這裏就說明插入成功了,否則在前面就返回false了
return true;
}
public static void TraverseTree(BitNode T){
if (T!= null){
TraverseTree(T.lchild);
System.out.println(T.getData());
TraverseTree(T.rchild);
}
}
}
高度標記類:
public class Status {
public boolean taller;
public Status(boolean taller) {
this.taller = taller;
}
}
測試類:
public class main {
public static void main(String[] args) {
int[] a = {62, 58, 88, 47, 73, 99, 35, 52, 93, 37};
BitNode T = new BitNode();
Status sta = new Status(false);
for (int i = 0; i < a.length; i++) {
AVLutils.InsertAVL(T, null, a[i], sta);
}
System.out.println("中序遍歷-------------");
AVLutils.TraverseTree(T);
}
}
多路查找樹(B樹)
多路查找樹(mutil-way search tree):其每一個結點的孩子數可以多與兩個,且每一個結點處可以存儲多個元素。
由於它是查找樹,所有元素之間存在某種特定的排序關係。
2-3樹
2-3:每一個結點都有兩個孩子(稱它爲2結點)或三個孩子(稱它爲3結點)
2結點:包含一個元素和兩個孩子,排序和二叉排序樹類似,但2結點要麼沒有孩子,要麼有兩個孩子。
3結點:包含一大一小兩個元素和三個孩子,左子樹包含小於較小元素的元素,中間子樹包含介於較小較大之間的元素,右子樹包含大於較大元素的元素。3結點要麼沒有孩子,要麼有兩個孩子。
性質:2-3樹中所有葉子都在同一層次。
2-3樹的插入:
- 空樹插入2結點即可
- 插入元素到2結點中。如下圖所示,3介於1,4之間,將左下角的2結點1改爲3結點1、3。
- 插入元素到3結點中。此時3結點元素已經滿了,所以要求修改3結點的父結點,把父結點改造成3結點。
4.插入元素到3結點的其他情況。如果父結點已經是3結點,那就繼續找父結點的父結點,直到找到2結點。
2-3樹的刪除:
- 刪除3結點上的葉子結點,直接和刪除即可
- 刪除2結點的葉子結點,可能會破壞2結點的定義,有四種情況在後面介紹
- 所刪除的元素位於非葉子的分支結點,通常是將樹按中序遍歷後得到此元素的前驅或後繼,考慮讓他們補位
刪除2結點的葉子結點有四種情況:
- 雙親是2結點,且擁有3結點的右孩子
- 雙親是2結點,右孩子也是2結點
- 雙親是一個3結點
- 如果當前樹是一個滿二叉樹的情況,此時刪除任何一個結點都不會滿足2-3結點的定義
2-3-4樹
概念:2-3樹的拓展,包含了4結點的使用,包含大中小三個元素和四個孩子,也是要麼四個要麼沒有。然後根據三個數分成四個區間,對應區間的數分配到對應的子樹中。
案例:
數組:{7,1,2,5,6,9,8,4,3}。
創建流程:
刪除流程,刪除順序是1、6、3、4、5、2、9:
B樹
B樹:B樹是一種平衡的多路查找樹,2-3樹和2-3-4樹都是B樹的特例。
B樹的階:結點最大的孩子數目。所以2-3樹是3階B樹,2-3-4樹是4階B樹。
一個m階的B樹有以下屬性:
- 如果根結點不是葉結點,則至少有兩棵子樹。
- 每一個非根的分支結點都有k-1個元素和k個孩子,其中;每一個葉結點n都有k-1個元素,其中。
- 所有葉子結點都位於同一層次。
B+樹(沒看)
這個沒代碼也不知道如何用,沒耐心看,先跳過
散列表(哈希表)查找概述
散列技術:在記錄的存儲位置和它的關鍵字之間建立一個確定的對應關係f,使得每個關鍵字key對應一個存儲位置f(key)。
散列函數/哈希函數:把這種對應關係f稱爲散列函數,又稱哈希函數。
散列表/哈希表:採用散列技術將記錄存儲在一塊連續的存儲空間中,這塊連續存儲空間稱爲散列表或哈希表。
散列表查找步驟:
- 根據散列函數計算地址,然後按地址存儲該記錄。
- 查找記錄時,根據同樣的散列函數計算記錄的地址,直接訪問地址。
適合場景:適合的求解問題是查找與給定值相等的記錄。
衝突:的時候,卻又,這種現象稱爲衝突,把這兩個關鍵字稱爲這個散列函數的同義詞。
散列函數的構造方法
散列函數的設計原則:
- 計算簡單,計算時間不應超過其他查找技術與關鍵字比較的時間。
- 散列地址分佈均勻,讓散列地址均勻分佈在存儲空間中,保證存儲空間的有效利用。
直接定址法:
取關鍵詞的某個線性函數值爲散列地址:
使用場景:需事先知道關鍵字分佈情況,適合查找表較小且連續的情況,由於這樣的限制,並不常用。
數組分析法:
抽取一部分,再進行反轉、移位、疊加等操作。
使用場景:處理關鍵字位數較大的情況,如果事先知道關鍵字的分佈且關鍵字的若干位分佈較均勻,就可以考慮用這個方法。
平方取中法:
假設關鍵字是1234,平方就是1522756,取中間三位是227,用作散列地址,也可以是275。
使用場景:不知道關鍵字的分佈,而位數又不是很大的情況。
摺疊法:
從左到右分割成位數相等的幾部分,最後一部分位數不夠可以短些,然後將這幾部分疊加求和,並按散列表表長,取後記爲做散列地址。
比如關鍵字是9876543210,散列表表長爲3爲,分成四組,987、654、321、0,然後疊加求和987+654+312+0=1962,再求後三位得散列地址962。
此時還不能保證分佈均勻,可以摺疊後再相加,如變成789+654+123+0=1566,此時地址爲566。
使用場景:事先不需要知道關鍵字的分佈,適合關鍵字位數較多的情況。
除留餘數法:
對於散列表長爲m的散列函數公式:
mod是取模的意思,不僅可以對關鍵字直接取模,也可在摺疊、平方取中後再取模。
比如 29 mod 12 = 5,就放在下標爲5的位置:
使用經驗:若表長爲m,通常p爲小於或等於表長(最好接近m)的最小質數或不包含小於20質因子的合數。
隨機數法:
不同方法的考慮因素:
- 計算地址所需時間
- 關鍵字的長度
- 散列表的大小
- 關鍵字的分佈情況
- 記錄查找的頻率
處理散列衝突的方法
開放定址法:
一旦發生了衝突,就去尋找下一個空的散列地址,只要散列表足夠大,空的散列地址總能找到,並將記錄存入。
衝突了就用下面的公式找新地址:
上面是線性探測法,但是會出現本來不是同義詞(第一次f的時候得到的值不相同)卻要爭奪一個地址的情況,這種現象稱爲堆積,但是堆積會大大降低存入和查找的效率。
因此提出二次探測法,目的是爲了不讓關鍵字都聚集在某一塊區域:
還有一種隨機探測法,因爲可設定隨機數種子,所以隨機數不會重複:
再散列函數法:
準備多個散列函數一起用,每當發生散列地址衝突的時候,就換一個計算方法,這樣做消耗的時間比較多:
鏈地址法:
將所有關鍵字爲同義詞的記錄存儲在一個單鏈表中,我們稱這種表爲同義詞子表,在散列表中值存儲所有同義詞子表的頭指針。
該方法一定會爲元素提供地址,但是會多出查找時遍歷單鏈表的性能損耗。
例如集合{12,67,56,16,25,37,22,29,15,47,48,34},以12爲出書,得表:
公共溢出區法:
創建一個地方專門存放衝突的關鍵字。
使用場景:適合衝突數據很少的情況,該結構對查找很友好。
散列表查找實現
定義散列表結構:
#define SUCCESS 1
#define UNSUCCESS 0
#define HASHSIZE 12 // 定義散列表長爲數組的長度
#define NULLKEY -32768
typedef struct HashTable
{
int *elem; // 元素存儲基址,動態分配數組
int count; // 當前數據元素個數
}
int m=0; // 散列表表長,全局變量
散列表的初始化:
Status Init HashTable(HashTable *H)
{
int i;
m = HASHSIZE;
H->count = m;
H->elem = (int *)malloc(m * sizeof(int));
for (i=0; i<m; i++)
H->elem[i] = NULLKEY;
return OK;
}
散列函數:
int Hash(int key)
{
return key % m; // 除留餘數法
}
插入操作:
插入的關鍵字集合:{12、67、56、16、25、37、22、29、15、47、48、34}
void InsertHash(HashTable *H, int key)
{
int addr = Hash(key);
while (H->elem[addr] != NULLKEY) // 不爲空,表衝突
addr = (addr + 1) %m; // 開放定址法的線性探測
H->elem[addr] = key; // 有空後插入關鍵字
}
查找記錄:
Status SearchHash(HashTable H, int key, int *addr)
{
*addr = Hash(key);
while(H.elem[*addr] != key) // 不相等則說明發生了衝突
{
*addr = (*addr + 1) % m;
if(H.elem[*addr]==NULLKEY || *addr==Hash(key)
{
//如果一直沒找到到了空地址,或者地址又轉回來了
return UNSUCCESS; // 說明關鍵字不存在
}
}
return SUCCESS;
}
散列表查找性能分析
如果沒有衝突的話複雜度就是,但是衝突是無法避免的,平均查找長度取決於以下幾個因素:
- 散列函數是否均勻。
- 處理衝突的方法。性能比較:鏈地址法 > 二次探測法 > 線性探測法
- 散列表的裝填因子。裝填因子=記錄個數 / 三列表長度。記錄越多,越大,產生衝突的可能性就越大。
總結:通常將散列表的空間設置得比查找集合大,雖然浪費了一定的空間,但是換來查找效率的大大提升。
JAVA實現散列表查找
public class main {
public static int HashSize = 12;
public static int NullKey = 65535;
public static void main(String[] args) {
// 初始化存儲數組
int[] save = new int[12];
for (int i = 0; i < HashSize; i++) {
save[i] = NullKey;
}
// 存放數組
int[] a = {2, 4, 55, 22, 550};
SaveIntoHash(save, a);
// 查找數組
System.out.println("------------開始查找----------");
int[] b = {2, 4, 55, 22, 550, 11};
for (int i = 0; i < b.length; i++) {
SearchInHash(save, b[i]);
}
}
private static void SearchInHash(int[] save, int key) {
boolean flag = true;
int add = getHash(key);
while (save[add]!=key){
add = (add + 1) % HashSize;
// 如過轉了一圈都沒找到
// 或者說add沒有再指向值,說明不是衝突而是不存在
if (save[add]==NullKey || add==getHash(key))
{
System.out.println("數組中不存在:" + key);
flag = false;
break;
}
}
if (flag){
System.out.println("找到了:" + key + " 地址爲:" + add);
}
}
private static void SaveIntoHash(int[] save, int[] a) {
for (int i = 0; i < a.length; i++) {
int add = getHash(a[i]);
// 如果發生衝突的話
while (save[add]!=NullKey){
add = (add + 1) % HashSize;
}
save[add] = a[i];
System.out.println("地址:" + add + " 值:" + a[i]);
}
}
public static int getHash(int k){
return k % HashSize;
}
}