當(dāng)前位置:首頁(yè) > 公眾號(hào)精選 > C語(yǔ)言與CPP編程
[導(dǎo)讀]時(shí)間、空間復(fù)雜度比較 查找算法 平均時(shí)間復(fù)雜度 空間復(fù)雜度 查找條件 順序查找 O(n) O(1) 無(wú)序或有序 二分查找(折半查找) O(log2n) O(1) 有序 插值查找 O(log2(log2n)) O(1) 有序 斐波那契查找 O(log2n) O(1) 有序 哈希查找 O(1) O(n) 無(wú)序或有序 二叉查找




時(shí)間、空間復(fù)雜度比較

查找算法 平均時(shí)間復(fù)雜度 空間復(fù)雜度 查找條件
順序查找 O(n) O(1) 無(wú)序或有序
二分查找(折半查找) O(log2n) O(1) 有序
插值查找 O(log2(log2n)) O(1) 有序
斐波那契查找 O(log2n) O(1) 有序
哈希查找 O(1) O(n) 無(wú)序或有序
二叉查找樹(二叉搜索樹查找) O(log2n)

紅黑樹 O(log2n)

2-3樹 O(log2n - log3n)

B樹/B+樹 O(log2n)

1 順序查找

算法思路

對(duì)于任意一個(gè)序列,從一端開始,順序掃描,依次將掃描到的結(jié)點(diǎn)關(guān)鍵字與給定值k相比較,若相等則表示查找成功;若掃描結(jié)束仍沒有找到關(guān)鍵字等于k的結(jié)點(diǎn),表示查找失敗。

代碼

#include <stdio.h>
#include <stdlib.h>
#define keyType int
//2020.05.24
typedef struct
{
keyType key;//查找表中每個(gè)數(shù)據(jù)元素的值
}ElemType;

typedef struct
{
ElemType *elem;//存放查找表中數(shù)據(jù)元素的數(shù)組
int length;//記錄查找表中數(shù)據(jù)的總數(shù)量
}SSTable;

//創(chuàng)建查詢數(shù)據(jù)
void Create(SSTable **st,int length)
{
(*st)=(SSTable*)malloc(sizeof(SSTable));
(*st)->length=length;
(*st)->elem =(ElemType*)malloc((length+1)*sizeof(ElemType));
printf("輸入表中的數(shù)據(jù)元素:\n");
//根據(jù)查找表中數(shù)據(jù)元素的總長(zhǎng)度,在存儲(chǔ)時(shí),從數(shù)組下標(biāo)為 1 的空間開始存儲(chǔ)數(shù)據(jù)
for (int i=1; i<=length; i++)
{
scanf("%d",&((*st)->elem[i].key));
}
}

//順序查找函數(shù),其中key為要查找的元素
int Search_seq(SSTable *str,keyType key)
{
//str->elem[0].key=key;//將關(guān)鍵字作為一個(gè)數(shù)據(jù)元素存放到查找表的第一個(gè)位置,起監(jiān)視哨的作用
int len = str->length;
//從最后一個(gè)數(shù)據(jù)元素依次遍歷,一直遍歷到數(shù)組下標(biāo)為0
for(int i=1; i<len+1; i++) //創(chuàng)建數(shù)據(jù)從數(shù)組下標(biāo)為1開始,查詢也從1開始
{
if(str->elem[i].key == key)
{
return i;
}
}
//如果 i=0,說(shuō)明查找失敗;查找成功返回要查找元素key的位置i
return 0;
}

int main()
{
SSTable *str;
int num;
printf("請(qǐng)輸入創(chuàng)建數(shù)據(jù)元素的個(gè)數(shù):\n");
scanf("%d",&num);
Create(&str, num);
getchar();
printf("請(qǐng)輸入要查找的數(shù)據(jù):\n");
int key;
scanf("%d",&key);
int location=Search_seq(str, key);
if (location==0) {
printf("查找失敗");
}else{
printf("要查找的%d的順序?yàn)椋?d",key,location);
}
return 0;
}

運(yùn)行結(jié)果

查找成功
查找失敗

2 二分查找(折半查找)

算法思路

  1. 確定查找范圍low=0,high=N-1,計(jì)算中項(xiàng)mid=(low+high)/2。

  2. 若mid==x或low>=high,則結(jié)束查找;否則,向下繼續(xù)。

  3. 若amid<x,說(shuō)明待查找的元素值只可能在比中項(xiàng)元素大的范圍內(nèi),則把mid+1的值賦給low,并重新計(jì)算mid,轉(zhuǎn)去執(zhí)行步驟2;若mid>x,說(shuō)明待查找的元素值只可能在比中項(xiàng)元素小的范圍內(nèi),則把mid-1的值賦給higt,并重新計(jì)算mid,轉(zhuǎn)去執(zhí)行步驟2。

說(shuō)明

  • 查找元素必須是有序的,如果是無(wú)序的則要先進(jìn)行排序操作。

  • 在做查找的過(guò)程中,如果 low 指針和 high 指針的中間位置在計(jì)算時(shí)位于兩個(gè)關(guān)鍵字中間,即求得 mid 的位置不是整數(shù),需要統(tǒng)一做取整操作。

折半查找的前提條件是需要有序表順序存儲(chǔ),對(duì)于靜態(tài)查找表,一次排序后不再變化,折半查找能得到不錯(cuò)的效率。但對(duì)于需要頻繁執(zhí)行插入或刪除操作的數(shù)據(jù)集來(lái)說(shuō),維護(hù)有序的排序會(huì)帶來(lái)不小的工作量,那就不建議使用。

                         ——《大話數(shù)據(jù)結(jié)構(gòu)》

代碼

#include <stdio.h>
#include <stdlib.h>
#define keyType int
typedef struct
{
keyType key;//查找表中每個(gè)數(shù)據(jù)元素的值
}ElemType;

typedef struct
{
ElemType *elem;//存放查找表中數(shù)據(jù)元素的數(shù)組
int length;//記錄查找表中數(shù)據(jù)的總數(shù)量
}SSTable;

//創(chuàng)建查詢數(shù)據(jù)
void Create(SSTable **st,int length)
{
(*st)=(SSTable*)malloc(sizeof(SSTable));
(*st)->length=length;
(*st)->elem =(ElemType*)malloc((length+1)*sizeof(ElemType));
printf("輸入表中的數(shù)據(jù)元素:\n");
//根據(jù)查找表中數(shù)據(jù)元素的總長(zhǎng)度,在存儲(chǔ)時(shí),從數(shù)組下標(biāo)為 1 的空間開始存儲(chǔ)數(shù)據(jù)
for (int i=1; i<=length; i++)
{
scanf("%d",&((*st)->elem[i].key));
}
}

//折半查找函數(shù) key為要查找的元素
int Search_Bin(SSTable *str,keyType key)
{
int low=1;//初始狀態(tài) low 指針指向第一個(gè)關(guān)鍵字
int high=str->length;//high 指向最后一個(gè)關(guān)鍵字
int mid;
while (low<=high)
{
mid=(low+high)/2;//int 本身為整形,所以,mid 每次為取整的整數(shù)
if(str->elem[mid].key==key)//如果 mid 指向的同要查找的相等,返回 mid 所指向的位置
{
return mid;
}
else if(str->elem[mid].key>key)//如果mid指向的關(guān)鍵字較大,則更新 high 指針的位置
{
high=mid-1;
}
//反之,則更新 low 指針的位置
else
{
low=mid+1;
}
}
return 0;
}

int main()
{
SSTable *str;
int num;
printf("請(qǐng)輸入創(chuàng)建數(shù)據(jù)元素的個(gè)數(shù):\n");
scanf("%d",&num);
Create(&str, num);
getchar();
printf("請(qǐng)輸入要查找的數(shù)據(jù):\n");
int key;
scanf("%d",&key);
int location=Search_Bin(str, key);
if (location==0) {
printf("沒有查找到");
}else{
printf("要查找的%d的順序?yàn)椋?d",key,location);
}
return 0;
}

運(yùn)行結(jié)果

查找成功
沒有查找到

3 插值查找

插值查找基于二分查找算法,將查找點(diǎn)的選擇改進(jìn)為自適應(yīng)選擇,可以提高查找效率。當(dāng)然,差值查找也屬于有序查找

算法思路

  1. 確定查找范圍low=0,high=N-1,計(jì)算中項(xiàng)mid=low+(key-a[low])/(a[high]-a[low])*(high-low)。

  2. 若mid==x或low>=high,則結(jié)束查找;否則,向下繼續(xù)。

  3. 若amid<x,說(shuō)明待查找的元素值只可能在比中項(xiàng)元素大的范圍內(nèi),則把mid+1的值賦給low,并重新計(jì)算mid,轉(zhuǎn)去執(zhí)行步驟2;若mid>x,說(shuō)明待查找的元素值只可能在比中項(xiàng)元素小的范圍內(nèi),則把mid-1的值賦給higt,并重新計(jì)算mid,轉(zhuǎn)去執(zhí)行步驟2。

說(shuō)明

  • 插值查找是基于折半查找進(jìn)行了優(yōu)化的查找方法。
  • 當(dāng)表長(zhǎng)較大,而關(guān)鍵字分布又比較均勻的查找表來(lái)說(shuō),插值查找算法的平均性能要比折半查找要好得多。

代碼

#include<stdio.h>

int array[10] = { 1, 4, 9, 16, 27, 31, 33, 35, 45, 64 };

int InsertionSearch(int data)
{
int low = 0;
int high = 10 - 1;
int mid = -1;
int comparisons = 1;
int index = -1;

while(low <= high)
{
printf("比較 %d 次\n" , comparisons );
printf("low : %d, list[%d] = %d\n", low, low, array[low]);
printf("high : %d, list[%d] = %d\n", high, high, array[high]);

comparisons++;
mid = low + (((double)(high - low) / (array[high] - array[low])) * (data - array[low]));
printf("mid = %d\n",mid);

// 沒有找到
if(array[mid] == data)
{
index = mid;
break;
}
else
{
if(array[mid] < data)
{
low = mid + 1;
}
else
{
high = mid - 1;
}
}
}

printf("比較次數(shù): %d\n", --comparisons);
return index;
}

int main()
{
int location = InsertionSearch(27); //測(cè)試代,查27,可以找到
if(location != -1)
{
printf("查找元素順序?yàn)? %d\n" ,(location+1));
}
else
{
printf("沒有查找到");
}
return 0;
}

運(yùn)行結(jié)果

運(yùn)行結(jié)果

4 斐波那契查找

斐波那契查找與折半查找很相似,他是根據(jù)斐波那契序列的特點(diǎn)對(duì)有序表進(jìn)行分割的。他要求開始表中記錄的個(gè)數(shù)為某個(gè)斐波那契數(shù)小1,及n=F(k)-1;開始將k值與第F(k-1)位置的記錄進(jìn)行比較(及mid=low+F(k-1)-1).

算法思路

  1. 相等,mid位置的元素即為所求

  2. 大于,low=mid+1,k-=2;

  3. 小于,high=mid-1,k-=1。

說(shuō)明

low=mid+1說(shuō)明待查找的元素在[mid+1,high]范圍內(nèi),k-=2 說(shuō)明范圍[mid+1,high]內(nèi)的元素個(gè)數(shù)為n-(F(k-1))= Fk-1-F(k-1)=Fk-F(k-1)-1=F(k-2)-1個(gè),所以可以遞歸的應(yīng)用斐波那契查找。

代碼

#include "stdafx.h"
#include <memory>
#include <iostream>
using namespace std;

const int max_size=20;//斐波那契數(shù)組的長(zhǎng)度

/*構(gòu)造一個(gè)斐波那契數(shù)組*/
void Fibonacci(int * F)
{
F[0]=0;
F[1]=1;
for(int i=2;i<max_size;++i)
F[i]=F[i-1]+F[i-2];
}

/*定義斐波那契查找法*/
int FibonacciSearch(int *a, int n, int key) //a為要查找的數(shù)組,n為要查找的數(shù)組長(zhǎng)度,key為要查找的關(guān)鍵字
{
int low=0;
int high=n-1;

int F[max_size];
Fibonacci(F);//構(gòu)造一個(gè)斐波那契數(shù)組F

int k=0;
while(n>F[k]-1)//計(jì)算n位于斐波那契數(shù)列的位置
++k;

int * temp;//將數(shù)組a擴(kuò)展到F[k]-1的長(zhǎng)度
temp=new int [F[k]-1];
memcpy(temp,a,n*sizeof(int));

for(int i=n;i<F[k]-1;++i)
temp[i]=a[n-1];

while(low<=high)
{
int mid=low+F[k-1]-1;
if(key<temp[mid])
{
high=mid-1;
k-=1;
}
else if(key>temp[mid])
{
low=mid+1;
k-=2;
}
else
{
if(mid<n)
return mid; //若相等則說(shuō)明mid即為查找到的位置
else
return n-1; //若mid>=n則說(shuō)明是擴(kuò)展的數(shù)值,返回n-1
}
}
delete [] temp;
return 0;
}

int main()
{
int a[] = {0,1,4,35,47,53,62,78,88,99};
int key=47;
int index=FibonacciSearch(a,sizeof(a)/sizeof(int),key);
if(index == 0)
{
cout<<"沒有找到"<<key;
}
else
{
cout<<key<<" 的位置是:"<<index;
}
return 0;
}

運(yùn)行結(jié)果

47的位置為5

5 哈希查找

哈希表

我們使用一個(gè)下標(biāo)范圍比較大的數(shù)組來(lái)存儲(chǔ)元素。可以設(shè)計(jì)一個(gè)函數(shù)(哈希函數(shù), 也叫做散列函數(shù)),使得每個(gè)元素的關(guān)鍵字都與一個(gè)函數(shù)值(即數(shù)組下標(biāo))相對(duì)應(yīng),于是用這個(gè)數(shù)組單元來(lái)存儲(chǔ)這個(gè)元素;也可以簡(jiǎn)單的理解為,按照關(guān)鍵字為每一個(gè)元素"分類",然后將這個(gè)元素存儲(chǔ)在相應(yīng)"類"所對(duì)應(yīng)的地方。但是,不能夠保證每個(gè)元素的關(guān)鍵字與函數(shù)值是一一對(duì)應(yīng)的,因此極有可能出現(xiàn)對(duì)于不同的元素,卻計(jì)算出了相同的函數(shù)值,這樣就產(chǎn)生了"沖突",換句話說(shuō),就是把不同的元素分在了相同的"類"之中。后面我們將看到一種解決"沖突"的簡(jiǎn)便做法。

"直接定址"與"解決沖突"是哈希表的兩大特點(diǎn)。

哈希函數(shù)

規(guī)則:通過(guò)某種轉(zhuǎn)換關(guān)系,使關(guān)鍵字適度的分散到指定大小的的順序結(jié)構(gòu)中,越分散,則以后查找的時(shí)間復(fù)雜度越小,空間復(fù)雜度越高。

算法思路

如果所有的鍵都是整數(shù),那么就可以使用一個(gè)簡(jiǎn)單的無(wú)序數(shù)組來(lái)實(shí)現(xiàn):將鍵作為索引,值即為其對(duì)應(yīng)的值,這樣就可以快速訪問(wèn)任意鍵的值。這是對(duì)于簡(jiǎn)單的鍵的情況,我們將其擴(kuò)展到可以處理更加復(fù)雜的類型的鍵。

  1. 用給定的哈希函數(shù)構(gòu)造哈希表;
  2. 根據(jù)選擇的沖突處理方法(常見方法:拉鏈法和線性探測(cè)法)解決地址沖突;
  3. 在哈希表的基礎(chǔ)上執(zhí)行哈希查找;

代碼

#include<stdio.h>
#include<stdlib.h>

#define SUCCESS 1
#define UNSUCCESS 0
#define OVERFLOW -1
#define OK 1
#define ERROR -1
#define MAXNUM 9999 // 用于初始化哈希表的記錄 key

typedef int Status;
typedef int KeyType;

// 哈希表中的記錄類型
typedef struct
{
KeyType key;
}RcdType;

// 哈希表類型
typedef struct
{
RcdType *rcd;
int size;
int count;
int *tag;
}HashTable;

// 哈希表每次重建增長(zhǎng)后的大小
int hashsize[] = { 11, 31, 61, 127, 251, 503 };
int index = 0;

// 初始哈希表
Status InitHashTable(HashTable &H, int size)
{
int i;
H.rcd = (RcdType *)malloc(sizeof(RcdType)*size);
H.tag = (int *)malloc(sizeof(int)*size);
if (NULL == H.rcd || NULL == H.tag) return OVERFLOW;
KeyType maxNum = MAXNUM;
for (i = 0; i < size; i++)
{
H.tag[i] = 0;
H.rcd[i].key = maxNum;
}
H.size = size;
H.count = 0;
return OK;
}

// 哈希函數(shù):除留余數(shù)法
int Hash(KeyType key, int m)
{
return (3 * key) % m;
}

// 處理哈希沖突:線性探測(cè)
void collision(int &p, int m)
{
p = (p + 1) % m;
}

// 在哈希表中查詢
Status SearchHash(HashTable H, KeyType key, int &p, int &c)
{
p = Hash(key, H.size);
int h = p;
c = 0;
while ((1 == H.tag[p] && H.rcd[p].key != key) || -1 == H.tag[p])
{
collision(p, H.size); c++;
}

if (1 == H.tag[p] && key == H.rcd[p].key) return SUCCESS;
else return UNSUCCESS;
}

//打印哈希表
void printHash(HashTable H)
{
int i;
printf("key : ");
for (i = 0; i < H.size; i++)
printf("%3d ", H.rcd[i].key);
printf("\n");
printf("tag : ");
for (i = 0; i < H.size; i++)
printf("%3d ", H.tag[i]);
printf("\n\n");
}

// 函數(shù)聲明:插入哈希表
Status InsertHash(HashTable &H, KeyType key);

// 重建哈希表
Status recreateHash(HashTable &H)
{
RcdType *orcd;
int *otag, osize, i;
orcd = H.rcd;
otag = H.tag;
osize = H.size;

InitHashTable(H, hashsize[index++]);
//把所有元素,按照新哈希函數(shù)放到新表中
for (i = 0; i < osize; i++)
{
if (1 == otag[i])
{
InsertHash(H, orcd[i].key);
}
}
return OK;
}

// 插入哈希表
Status InsertHash(HashTable &H, KeyType key)
{
int p, c;
if (UNSUCCESS == SearchHash(H, key, p, c))
{ //沒有相同key
if (c*1.0 / H.size < 0.5)
{ //沖突次數(shù)未達(dá)到上線
//插入代碼
H.rcd[p].key = key;
H.tag[p] = 1;
H.count++;
return SUCCESS;
}
else recreateHash(H); //重構(gòu)哈希表
}
return UNSUCCESS;
}

// 刪除哈希表
Status DeleteHash(HashTable &H, KeyType key)
{
int p, c;
if (SUCCESS == SearchHash(H, key, p, c))
{
//刪除代碼
H.tag[p] = -1;
H.count--;
return SUCCESS;
}
else return UNSUCCESS;
}

int main()
{
printf("-----哈希表-----\n");
HashTable H;
int i;
int size = 11;
KeyType array[8] = { 22, 41, 53, 46, 30, 13, 12, 67 };
KeyType key;

//初始化哈希表
printf("初始化哈希表\n");
if (SUCCESS == InitHashTable(H, hashsize[index++])) printf("初始化成功\n");

//插入哈希表
printf("插入哈希表\n");
for (i = 0; i <= 7; i++)
{
key = array[i];
InsertHash(H, key);
printHash(H);
}

//刪除哈希表
printf("刪除哈希表中key為12的元素\n");
int p, c;
if (SUCCESS == DeleteHash(H, 12))
{
printf("刪除成功,此時(shí)哈希表為:\n");
printHash(H);
}

//查詢哈希表
printf("查詢哈希表中key為67的元素\n");
if (SUCCESS == SearchHash(H, 67, p, c)) printf("查詢成功\n");

//再次插入,測(cè)試哈希表的重建
printf("再次插入,測(cè)試哈希表的重建:\n");
KeyType array1[8] = { 27, 47, 57, 47, 37, 17, 93, 67 };
for (i = 0; i <= 7; i++)
{
key = array1[i];
InsertHash(H, key);
printHash(H);
}

getchar();
return 0;
}

6 二叉樹查找

二叉查找樹是先對(duì)待查找的數(shù)據(jù)進(jìn)行生成樹,確保樹的左分支的值小于右分支的值,然后在就行和每個(gè)節(jié)點(diǎn)的父節(jié)點(diǎn)比較大小,查找最適合的范圍。這個(gè)算法的查找效率很高,但是如果使用這種查找方法要首先創(chuàng)建樹。

算法思路

  1. 若b是空樹,則搜索失?。?
  2. 若x等于b的根節(jié)點(diǎn)的數(shù)據(jù)域之值,則查找成功:
  3. 若x小于b的根節(jié)點(diǎn)的數(shù)據(jù)域之值,則搜索左子樹:
  4. 查找右子樹。

代碼

遞歸和非遞歸

//非遞歸查找算法
BSTNode *BST_Search(BiTree T,ElemType key,BSTNode *&p)
{
//查找函數(shù)返回指向關(guān)鍵字值為key的結(jié)點(diǎn)指針,不存在則返回NULL
p=NULL;
while(T!=NULL&&key!=T->data)
{
p=T; //指向被查找結(jié)點(diǎn)的雙親
if(key<T->data)
T=T->lchild; //查找左子樹
else
T=T->rchild; //查找右子樹
}
return T;
}

//遞歸算法
Status Search_BST(BiTree T, int key, BiTree f, BiTree *p)
{
//查找BST中是否存在key,f指向T雙親,其初始值為NULL
//若查找成功,指針p指向數(shù)據(jù)元素結(jié)點(diǎn),返回true;
//若失敗,p指向查找路徑上訪問(wèn)的最后一個(gè)結(jié)點(diǎn)并返回false
if(!T)
{
*p=f;
return false;
}
else if(key==T->data)
{ //查找成功
*p=T;
return true;
}
else if(key<T->data)
return Search_BST(T->lchild,key,T,p); //遞歸查找左子樹
else
return Search_BST(T->rchild,key,T,p); //遞歸查找右子樹

}

7 2-3樹

2-3樹運(yùn)行每個(gè)節(jié)點(diǎn)保存1個(gè)或者兩個(gè)的值。對(duì)于普通的2節(jié)點(diǎn)(2-node),他保存1個(gè)key和左右兩個(gè)自己點(diǎn)。對(duì)應(yīng)3節(jié)點(diǎn)(3-node),保存兩個(gè)Key,2-3查找樹的定義如下:

  1. 要么為空,要么:

  2. 對(duì)于2節(jié)點(diǎn),該節(jié)點(diǎn)保存一個(gè)key及對(duì)應(yīng)value,以及兩個(gè)指向左右節(jié)點(diǎn)的節(jié)點(diǎn),左節(jié)點(diǎn)也是一個(gè)2-3節(jié)點(diǎn),所有的值都比key要小,右節(jié)點(diǎn)也是一個(gè)2-3節(jié)點(diǎn),所有的值比key要大。

  3. 對(duì)于3節(jié)點(diǎn),該節(jié)點(diǎn)保存兩個(gè)key及對(duì)應(yīng)value,以及三個(gè)指向左中右的節(jié)點(diǎn)。左節(jié)點(diǎn)也是一個(gè)2-3節(jié)點(diǎn),所有的值均比兩個(gè)key中的最小的key還要??;中間節(jié)點(diǎn)也是一個(gè)2-3節(jié)點(diǎn),中間節(jié)點(diǎn)的key值在兩個(gè)跟節(jié)點(diǎn)key值之間;右節(jié)點(diǎn)也是一個(gè)2-3節(jié)點(diǎn),節(jié)點(diǎn)的所有key值比兩個(gè)key中的最大的key還要大。

算法思路:

要判斷一個(gè)鍵是否在樹中,我們先將它和根結(jié)點(diǎn)中的鍵比較。如果它和其中的任何一個(gè)相等,查找命中。否則我們就根據(jù)比較的結(jié)果找到指向相應(yīng)區(qū)間的鏈接,并在其指向的子樹中遞歸地繼續(xù)查找。如果這是個(gè)空鏈接,查找未命中。

2-3 樹中查找鍵為H的節(jié)點(diǎn)
2-3 樹中查找鍵為B的節(jié)點(diǎn)

代碼

two_three *search23(two_three *t, element x)
{
while(t)
{
if (x < t->data_l)
{
t = t->left_child;
}
else if (x > t->data_l && x < t->data_r)
{
t = t->middle_child;
}
else if (x > t->data_r)
{
t = t->right_child;
}
else
{
return t;
}
}
return NULL;
}

8 紅黑樹

2-3查找樹能保證在插入元素之后能保持樹的平衡狀態(tài),最壞情況下即所有的子節(jié)點(diǎn)都是2-node,樹的高度為lgn,從而保證了最壞情況下的時(shí)間復(fù)雜度。但是2-3樹實(shí)現(xiàn)起來(lái)比較復(fù)雜,于是就有了一種簡(jiǎn)單實(shí)現(xiàn)2-3樹的數(shù)據(jù)結(jié)構(gòu),即紅黑樹(Red-Black Tree)。

理解紅黑樹一句話就夠了:紅黑樹就是用紅鏈接表示3-結(jié)點(diǎn)的2-3樹。

現(xiàn)在我們對(duì)2-3樹進(jìn)行改造,改造成一個(gè)二叉樹。怎么改造呢?對(duì)于2節(jié)點(diǎn),保持不變;對(duì)于3節(jié)點(diǎn),我們首先將3節(jié)點(diǎn)中左側(cè)的元素標(biāo)記為紅色,然后我們將其改造成圖3的形式;

再將3節(jié)點(diǎn)的位于中間的子節(jié)點(diǎn)的父節(jié)點(diǎn)設(shè)置為父節(jié)點(diǎn)中那個(gè)紅色的節(jié)點(diǎn),如圖4的所示;最后我們將圖4的形式改為二叉樹的樣子,如圖5所示。圖5是不是很熟悉,沒錯(cuò),這就是我們常常提到的大名鼎鼎的紅黑樹了。如下圖所示。

2-3樹轉(zhuǎn)紅黑樹

為什么使用紅黑樹

  • 紅黑樹是一種平衡樹,他復(fù)雜的定義和規(guī)則都是為了保證樹的平衡性。如果樹不保證他的平衡性就是下圖:很顯然這就變成一個(gè)鏈表了。
  • 保證平衡性的最大的目的就是降低樹的高度,因?yàn)闃涞牟檎倚阅苋Q于樹的高度。所以樹的高度越低搜索的效率越高!
  • 這也是為什么存在二叉樹、搜索二叉樹等,各類樹的目的。

紅黑樹性質(zhì)

  • 每個(gè)節(jié)點(diǎn)要么是黑色,要么是紅色。
  • 根節(jié)點(diǎn)是黑色。
  • 每個(gè)葉子節(jié)點(diǎn)(NIL)是黑色。
  • 每個(gè)紅色結(jié)點(diǎn)的兩個(gè)子結(jié)點(diǎn)一定都是黑色。
  • 任意一結(jié)點(diǎn)到每個(gè)葉子結(jié)點(diǎn)的路徑都包含數(shù)量相同的黑結(jié)點(diǎn)。

算法思路

紅黑樹的思想就是對(duì)2-3查找樹進(jìn)行編碼,尤其是對(duì)2-3查找樹中的3-nodes節(jié)點(diǎn)添加額外的信息。紅黑樹中將節(jié)點(diǎn)之間的鏈接分為兩種不同類型,紅色鏈接,他用來(lái)鏈接兩個(gè)2-nodes節(jié)點(diǎn)來(lái)表示一個(gè)3-nodes節(jié)點(diǎn)。黑色鏈接用來(lái)鏈接普通的2-3節(jié)點(diǎn)。特別的,使用紅色鏈接的兩個(gè)2-nodes來(lái)表示一個(gè)3-nodes節(jié)點(diǎn),并且向左傾斜,即一個(gè)2-node是另一個(gè)2-node的左子節(jié)點(diǎn)。這種做法的好處是查找的時(shí)候不用做任何修改,和普通的二叉查找樹相同。

代碼

#define BLACK 1
#define RED 0
#include <iostream>

using namespace std;

class bst
{
private:

struct Node
{
int value;
bool color;
Node *leftTree, *rightTree, *parent;

Node() : value(0), color(RED), leftTree(NULL), rightTree(NULL), parent(NULL) { }

Node* grandparent()
{
if (parent == NULL)
{
return NULL;
}
return parent->parent;
}

Node* uncle()
{
if (grandparent() == NULL)
{
return NULL;
}
if (parent == grandparent()->rightTree)
return grandparent()->leftTree;
else
return grandparent()->rightTree;
}

Node* sibling()
{
if (parent->leftTree == this)
return parent->rightTree;
else
return parent->leftTree;
}
};

void rotate_right(Node *p)
{
Node *gp = p->grandparent();
Node *fa = p->parent;
Node *y = p->rightTree;

fa->leftTree = y;

if (y != NIL)
y->parent = fa;
p->rightTree = fa;
fa->parent = p;

if (root == fa)
root = p;
p->parent = gp;

if (gp != NULL)
{
if (gp->leftTree == fa)
gp->leftTree = p;
else
gp->rightTree = p;
}

}

void rotate_left(Node *p)
{
if (p->parent == NULL)
{
root = p;
return;
}
Node *gp = p->grandparent();
Node *fa = p->parent;
Node *y = p->leftTree;

fa->rightTree = y;

if (y != NIL)
y->parent = fa;
p->leftTree = fa;
fa->parent = p;

if (root == fa)
root = p;
p->parent = gp;

if (gp != NULL)
{
if (gp->leftTree == fa)
gp->leftTree = p;
else
gp->rightTree = p;
}
}

void inorder(Node *p)
{
if (p == NIL)
return;

if (p->leftTree)
inorder(p->leftTree);

cout << p->value << " ";

if (p->rightTree)
inorder(p->rightTree);
}

string outputColor(bool color)
{
return color ? "BLACK" : "RED";
}

Node* getSmallestChild(Node *p)
{
if (p->leftTree == NIL)
return p;
return getSmallestChild(p->leftTree);
}

bool delete_child(Node *p, int data)
{
if (p->value > data)
{
if (p->leftTree == NIL)
{
return false;
}
return delete_child(p->leftTree, data);
}
else if (p->value < data)
{
if (p->rightTree == NIL)
{
return false;
}
return delete_child(p->rightTree, data);
}
else if (p->value == data)
{
if (p->rightTree == NIL)
{
delete_one_child(p);
return true;
}
Node *smallest = getSmallestChild(p->rightTree);
swap(p->value, smallest->value);
delete_one_child(smallest);

return true;
}
else
{
return false;
}
}

void delete_one_child(Node *p)
{
Node *child = p->leftTree == NIL ? p->rightTree : p->leftTree;
if (p->parent == NULL && p->leftTree == NIL && p->rightTree == NIL)
{
p = NULL;
root = p;
return;
}

if (p->parent == NULL)
{
delete p;
child->parent = NULL;
root = child;
root->color = BLACK;
return;
}

if (p->parent->leftTree == p)
{
p->parent->leftTree = child;
}
else
{
p->parent->rightTree = child;
}
child->parent = p->parent;

if (p->color == BLACK)
{
if (child->color == RED)
{
child->color = BLACK;
}
else
delete_case(child);
}

delete p;
}

void delete_case(Node *p)
{
if (p->parent == NULL)
{
p->color = BLACK;
return;
}
if (p->sibling()->color == RED)
{
p->parent->color = RED;
p->sibling()->color = BLACK;
if (p == p->parent->leftTree)
rotate_left(p->sibling());
else
rotate_right(p->sibling());
}
if (p->parent->color == BLACK && p->sibling()->color == BLACK
&& p->sibling()->leftTree->color == BLACK && p->sibling()->rightTree->color == BLACK)
{
p->sibling()->color = RED;
delete_case(p->parent);
}
else if (p->parent->color == RED && p->sibling()->color == BLACK
&& p->sibling()->leftTree->color == BLACK && p->sibling()->rightTree->color == BLACK)
{
p->sibling()->color = RED;
p->parent->color = BLACK;
}
else
{
if (p->sibling()->color == BLACK)
{
if (p == p->parent->leftTree && p->sibling()->leftTree->color == RED
&& p->sibling()->rightTree->color == BLACK)
{
p->sibling()->color = RED;
p->sibling()->leftTree->color = BLACK;
rotate_right(p->sibling()->leftTree);
}
else if (p == p->parent->rightTree && p->sibling()->leftTree->color == BLACK
&& p->sibling()->rightTree->color == RED)
{
p->sibling()->color = RED;
p->sibling()->rightTree->color = BLACK;
rotate_left(p->sibling()->rightTree);
}
}
p->sibling()->color = p->parent->color;
p->parent->color = BLACK;
if (p == p->parent->leftTree)
{
p->sibling()->rightTree->color = BLACK;
rotate_left(p->sibling());
}
else
{
p->sibling()->leftTree->color = BLACK;
rotate_right(p->sibling());
}
}
}

void insert(Node *p, int data)
{
if (p->value >= data)
{
if (p->leftTree != NIL)
insert(p->leftTree, data);
else
{
Node *tmp = new Node();
tmp->value = data;
tmp->leftTree = tmp->rightTree = NIL;
tmp->parent = p;
p->leftTree = tmp;
insert_case(tmp);
}
}
else
{
if (p->rightTree != NIL)
insert(p->rightTree, data);
else
{
Node *tmp = new Node();
tmp->value = data;
tmp->leftTree = tmp->rightTree = NIL;
tmp->parent = p;
p->rightTree = tmp;
insert_case(tmp);
}
}
}

void insert_case(Node *p)
{
if (p->parent == NULL)
{
root = p;
p->color = BLACK;
return;
}
if (p->parent->color == RED)
{
if (p->uncle()->color == RED)
{
p->parent->color = p->uncle()->color = BLACK;
p->grandparent()->color = RED;
insert_case(p->grandparent());
}
else
{
if (p->parent->rightTree == p && p->grandparent()->leftTree == p->parent)
{
rotate_left(p);
rotate_right(p);
p->color = BLACK;
p->leftTree->color = p->rightTree->color = RED;
}
else if (p->parent->leftTree == p && p->grandparent()->rightTree == p->parent)
{
rotate_right(p);
rotate_left(p);
p->color = BLACK;
p->leftTree->color = p->rightTree->color = RED;
}
else if (p->parent->leftTree == p && p->grandparent()->leftTree == p->parent)
{
p->parent->color = BLACK;
p->grandparent()->color = RED;
rotate_right(p->parent);
}
else if (p->parent->rightTree == p && p->grandparent()->rightTree == p->parent)
{
p->parent->color = BLACK;
p->grandparent()->color = RED;
rotate_left(p->parent);
}
}
}
}

void DeleteTree(Node *p)
{
if (!p || p == NIL)
{
return;
}
DeleteTree(p->leftTree);
DeleteTree(p->rightTree);
delete p;
}
public:

bst()
{
NIL = new Node();
NIL->color = BLACK;
root = NULL;
}

~bst()
{
if (root)
DeleteTree(root);
delete NIL;
}

void inorder()
{
if (root == NULL)
return;
inorder(root);
cout << endl;
}

void insert(int x)
{
if (root == NULL)
{
root = new Node();
root->color = BLACK;
root->leftTree = root->rightTree = NIL;
root->value = x;
}
else
{
insert(root, x);
}
}

bool delete_value(int data)
{
return delete_child(root, data);
}
private:
Node *root, *NIL;
};

int main()
{
cout << "---【紅黑樹】---" << endl;
// 創(chuàng)建紅黑樹
bst tree;

// 插入元素
tree.insert(2);
tree.insert(9);
tree.insert(-10);
tree.insert(0);
tree.insert(33);
tree.insert(-19);

// 順序打印紅黑樹
cout << "插入元素后的紅黑樹:" << endl;
tree.inorder();

// 刪除元素
tree.delete_value(2);

// 順序打印紅黑樹
cout << "刪除元素 2 后的紅黑樹:" << endl;
tree.inorder();

// 析構(gòu)
tree.~bst();

getchar();
return 0;
}

9 B樹/B+樹

在計(jì)算機(jī)科學(xué)中,B樹(B-tree)是一種樹狀數(shù)據(jù)結(jié)構(gòu),它能夠存儲(chǔ)數(shù)據(jù)、對(duì)其進(jìn)行排序并允許以O(shè)(log n)的時(shí)間復(fù)雜度運(yùn)行進(jìn)行查找、順序讀取、插入和刪除的數(shù)據(jù)結(jié)構(gòu)。B樹,概括來(lái)說(shuō)是一個(gè)節(jié)點(diǎn)可以擁有多于2個(gè)子節(jié)點(diǎn)的二叉查找樹。與自平衡二叉查找樹不同,B-樹為系統(tǒng)最優(yōu)化大塊數(shù)據(jù)的讀和寫操作。B-tree算法減少定位記錄時(shí)所經(jīng)歷的中間過(guò)程,從而加快存取速度。普遍運(yùn)用在數(shù)據(jù)庫(kù)和文件系統(tǒng)。

                             ——維基百科

B 樹可以看作是對(duì)2-3查找樹的一種擴(kuò)展,即他允許每個(gè)節(jié)點(diǎn)有M-1個(gè)子節(jié)點(diǎn)。

  • 定義任意非葉子結(jié)點(diǎn)最多只有M個(gè)兒子;且M>2;
  • 根結(jié)點(diǎn)的兒子數(shù)為[2, M];
  • 除根結(jié)點(diǎn)以外的非葉子結(jié)點(diǎn)的兒子數(shù)為[M/2, M];
  • 每個(gè)結(jié)點(diǎn)存放至少M(fèi)/2-1(取上整)和至多M-1個(gè)關(guān)鍵字;(至少2個(gè)關(guān)鍵字)
  • 非葉子結(jié)點(diǎn)的關(guān)鍵字:K[1], K[2], …, K[M-1];且K[i] < K[i+1];
  • 非葉子結(jié)點(diǎn)的指針:P[1], P[2], …, P[M];其中P[1]指向關(guān)鍵字小于K[1]的 子樹,P[M]指向關(guān)鍵字大于K[M-1]的子樹,其它P[i]指向關(guān)鍵字屬于(K[i-1], K[i])的子樹;
  • 所有葉子結(jié)點(diǎn)位于同一層;

如:(M=3)

算法思路

從根結(jié)點(diǎn)開始,對(duì)結(jié)點(diǎn)內(nèi)的關(guān)鍵字(有序)序列進(jìn)行二分查找,如果命中則結(jié)束,否則進(jìn)入查詢關(guān)鍵字所屬范圍的兒子結(jié)點(diǎn);重復(fù),直到所對(duì)應(yīng)的兒子指針為空,或已經(jīng)是葉子結(jié)點(diǎn);

  • 關(guān)鍵字集合分布在整顆樹中;
  • 任何一個(gè)關(guān)鍵字出現(xiàn)且只出現(xiàn)在一個(gè)結(jié)點(diǎn)中;
  • 搜索有可能在非葉子結(jié)點(diǎn)結(jié)束;
  • 其搜索性能等價(jià)于在關(guān)鍵字全集內(nèi)做一次二分查找;
  • 自動(dòng)層次控制;

代碼

BTNode* BTree_recursive_search(const BTree tree, KeyType key, int* pos)
{
int i = 0;
while (i < tree->keynum && key > tree->key[i])
{
++i;
}

// 查找關(guān)鍵字
if (i < tree->keynum && tree->key[i] == key)
{
*pos = i;
return tree;
}

// tree 為葉子節(jié)點(diǎn),找不到 key,查找失敗返回
if (tree->isLeaf)
{
return NULL;
}

// 節(jié)點(diǎn)內(nèi)查找失敗,但 tree->key[i - 1]< key < tree->key[i],
// 下一個(gè)查找的結(jié)點(diǎn)應(yīng)為 child[i]

// 從磁盤讀取第 i 個(gè)孩子的數(shù)據(jù)
disk_read(&tree->child[i]);

// 遞歸地繼續(xù)查找于樹 tree->child[i]
return BTree_recursive_search(tree->child[i], key, pos);
}

B+樹

B+樹是B樹的變體,也是一種多路搜索樹:

其定義基本與B-樹同,除了:

  • 非葉子結(jié)點(diǎn)的子樹指針與關(guān)鍵字個(gè)數(shù)相同;
  • 非葉子結(jié)點(diǎn)的子樹指針P[i],指向關(guān)鍵字值屬于[K[i], K[i+1])的子樹, B樹是開區(qū)間
  • 為所有葉子結(jié)點(diǎn)增加一個(gè)鏈指針;
  • 所有關(guān)鍵字都在葉子結(jié)點(diǎn)出現(xiàn);

如:(M=3)

1

算法思路

B+的搜索與B樹也基本相同,區(qū)別是B+樹只有達(dá)到葉子結(jié)點(diǎn)才命中(B樹可以在 非葉子結(jié)點(diǎn)命中),其性能也等價(jià)于在關(guān)鍵字全集做一次二分查找;

  • 所有關(guān)鍵字都出現(xiàn)在葉子結(jié)點(diǎn)的鏈表中(稠密索引),且鏈表中的關(guān)鍵字恰好是有序的;
  • 不可能在非葉子結(jié)點(diǎn)命中;
  • 非葉子結(jié)點(diǎn)相當(dāng)于是葉子結(jié)點(diǎn)的索引(稀疏索引),葉子結(jié)點(diǎn)相當(dāng)于是存儲(chǔ)(關(guān)鍵字)數(shù)據(jù)的數(shù)據(jù)層;
  • 更適合文件索引系統(tǒng);

參考資料

  1. https://www.sohu.com/a/296278527_478315
  2. https://www.cnblogs.com/exzlc/p/12203744.html
  3. 部分配圖來(lái)源于網(wǎng)絡(luò)

最近原創(chuàng)推薦

如何定義一個(gè)只能在(堆/棧)上生成對(duì)象的類

十大經(jīng)典排序算法(動(dòng)態(tài)演示+代碼)

C語(yǔ)言與C++面試知識(shí)總結(jié)
數(shù)據(jù)結(jié)構(gòu)之堆棧

一文輕松理解內(nèi)存對(duì)齊

一文讀懂C語(yǔ)言與C++動(dòng)態(tài)內(nèi)存

面試中常見的C語(yǔ)言與C++區(qū)別的問(wèn)題

數(shù)據(jù)結(jié)構(gòu)之線性表


免責(zé)聲明:本文內(nèi)容由21ic獲得授權(quán)后發(fā)布,版權(quán)歸原作者所有,本平臺(tái)僅提供信息存儲(chǔ)服務(wù)。文章僅代表作者個(gè)人觀點(diǎn),不代表本平臺(tái)立場(chǎng),如有問(wèn)題,請(qǐng)聯(lián)系我們,謝謝!

本站聲明: 本文章由作者或相關(guān)機(jī)構(gòu)授權(quán)發(fā)布,目的在于傳遞更多信息,并不代表本站贊同其觀點(diǎn),本站亦不保證或承諾內(nèi)容真實(shí)性等。需要轉(zhuǎn)載請(qǐng)聯(lián)系該專欄作者,如若文章內(nèi)容侵犯您的權(quán)益,請(qǐng)及時(shí)聯(lián)系本站刪除。
關(guān)閉
關(guān)閉