數據結構復習
一個月學完數據結構,寫一篇究極總結,當復習了
棧(Stack)
回歸原始,可以用鏈表實現棧,或者說所有的數據結構都可以用鏈表實現。STL中用stack
容器實現棧。棧的特征就是先進后出,First In Last Out。
template <class T, class Container = deque
> class stack;

T就是棧所存的元素類型,container是容器適配器,不寫第二個參數的情況下默認是deque,你也可以自己把它換成vector,具體搜索容器適配器,這里不多贅述。
需要注意的一點是,stack沒有迭代器,不允許遍歷,只能在棧頂進行push
、top
、pop
操作。
常用函數
size()
返回棧內元素數量,類型是unsigned integral。
empty()
用於判斷棧內有無元素(stack.size() == 0),是否為空,返回bool值。
top()
用於返回棧頂元素的引用。
push(value_type)
和emplace(arg...)
都是在棧頂插入元素,區別在於push
是直接插入對象,在傳參數的時候是一個對象。而emplace
可以跟push
一樣,直接插入對象,也可以直接傳入構造對象需要的元素,然后自己調用其構造函數。這么說應該說不明白,舉個例子:
我有一個類
class Data {
private:
int a, b;
public:
Data(int x, int y) :a(x), b(y) {};
};
現在聲明一個棧,類型是Data
stack<Data> nums;
下面分別用push
和emplace
插入元素
nums.push(Data(1, 2)); //先調用Data的構造函數,創建出對象之后再壓入棧頂,參數是個右值
nums.emplace(Data(1, 2)); //同上
nums.emplace(1, 2); //先調用Data的構造函數,傳進去的參數用於Data的構造函數,把對象建好之后再壓入棧
可以看出來emplace
擁有push
一樣的功能,但是它還能調用對象的構造函數,十分強大。
pop();
用於刪除棧頂元素,沒有返回值。
swap(stack2)
用於將一個堆棧的內容與另一個相同類型的堆棧交換,但是大小可能會有所不同,沒有返回值。
隊(Queue)
STL中使用queue
容器實現棧。跟棧不一樣,隊是先進先出,First In First Out。
template <class T, class Container = deque
> class queue;

跟stack
一樣,T是容器的類型,container是適配器,默認deque。queue
同樣沒有迭代器,只能通過一些函數在隊首、隊尾操作。
STL的queue
就不說了,用起來跟stack
差不多,一些api百度一下就好了。
循環隊列
跟棧不一樣的是,隊還有一個問題:循環隊列。想像一下,一條蛇圍成一個圓,嘴巴咬着尾巴,這個就是循環隊列的樣子。

下面寫一下循環隊列:
template <class T>
class SeqQueue {
public:
SeqQueue(int sz = 10); // 構造函數
~SeqQueue() { // 析構函數
delete[] elements;
};
bool EnQueue(const T& x); // 若隊列不滿,則將x進隊,否則隊溢出處理。
bool DeQueue(T& x); // 若隊列不空,則退出隊頭元素x並由函數返回true,否則隊空,返回false
bool getFront(T& x)const; // 若隊列不為空,則函數返回true及隊頭元素的值,否則返回false。
void makeEmpty() { // 置空操作:隊頭指針和隊尾指針置0
front = rear = 0;
};
bool IsEmpty()const { // 判斷列空否。若隊列空,則函數返回true,否則返回false
return(front == rear) ? true : false;
};
bool IsFull()const { // 判斷隊列滿否。若隊列滿,則函數返回true,否則返回false
return ((rear + 1) % maxSize == front) ? true : false;
};
int getSize()const { // 求隊列元素個數
return (rear - front + maxSize) % maxSize;
};
protected:
int rear, front; // 隊尾與隊頭指針
T* elements; // 存放隊列元素的數組
int maxSize; // 隊列最大可容納元素個數
};
template <class T>
SeqQueue<T>::SeqQueue(int sz) :front(0), rear(0), maxSize(sz) {
// 建立一個最大具有maxSize個元素的空隊列。
elements = new T[maxSize]; // 創建隊列空間
};
template <class T>
bool SeqQueue<T>::EnQueue(const T& x) {
// 若隊列不滿,則將元素x插入到該隊列的隊尾,否則出錯處理。
if (IsFull() == true) { // 隊列滿則插入失敗,返回
return false;
}
elements[rear] = x; // 按照隊尾指針指示位置插入
rear = (rear + 1) % maxSize; // 隊尾指針加1
return true; // 插入成功,返回
};
template <class T>
bool SeqQueue<T>::DeQueue(T& x) {
// 若隊列不空則函數退掉一個隊頭元素並返回true,否則函數返回false
if (IsEmpty() == true) { // 若隊列空則函數返回空指針
return false;
}
x = elements[front];
front = (front + 1) % maxSize; // 隊頭指針加1
return true; // 刪除成功,返回
};
template<class T>
bool SeqQueue<T>::getFront(T& x)const {
// 若隊列不空則函數返回該隊列隊頭元素的值
if (IsEmpty() == true) { // 若隊列空則函數返回空指針
return false;
}
x = elements[front]; // 返回隊頭元素的值
return true;
};
把大致功能都實現了一遍,代碼有參考。總結一下循環隊列的關鍵點:
- 循環列表最關鍵的是處理隊頭隊尾指針,不能一直加一直減,所以就用到了求余的方法
- 判斷是否為空:front == rear
- 判斷是否已滿:(rear + 1) % maxSize == front
- 求元素個數:(rear - front + maxSize) % maxSize
哈希表(Hash Table)
哈希表又叫散列表,它儲存數據是通過鍵值對進行的。關於鍵值對,可以想象你在逛商場買衣服,每一件衣服上都掛着一個吊牌,你每次都是拿吊牌,然后吊牌的繩子把衣服拉起來,吊牌就是鍵,衣服就是值。但是哈希表最大的不同在於,它的鍵是經過處理的:哈希表的鍵通過散列函數映射到某一集合,比如f(key)=a*key+b
,通過散列函數映射之后得到的鍵就不是原來的鍵了。根據哈希表的種種特征,很容易看出來哈希表里面沒有重復的鍵,但是一個鍵可以對應多個值,具體看你怎么使用了。

你可能會問這樣做有什么用,試想一些鍵非常大,或者很分散,你存起來會消耗很多內存,耗時也久。但是經過散列函數映射到一個范圍里面,就能加快效率,減少開銷。但是哈希表這么好用,自然也會有缺陷的:萬一經過散列函數之后,某一些鍵映射到同一個地方怎么辦?這就是哈希沖突。
解決的辦法也有很多,開放尋址法(線性探測法、平方探測法)、鏈地址法、再散列法等。
稍微寫一下簡單的哈希表:
#define MAXTABLESIZE 10000
#define KEYLENGTH 100
struct LNode {
int data;
LNode* next;
LNode() :data(0), next(nullptr) {};
};
struct HashTable {
int tablesize;
LNode* heads;
HashTable() :tablesize(0), heads(nullptr) {};
};
/// <summary>
/// 返回大於n且不超過MAXTABLESIZE的最小質數
/// </summary>
/// <param name="n">起始值</param>
/// <returns>質數</returns>
int NextPrime(int n) {
int p = (n % 2) ? n + 2 : n + 1;
int i = 0;
while (p <= MAXTABLESIZE) {
for (i = (int)sqrt(p); i > 2; i--) {
if ((p % i) == 0) {
break;
}
}
if (i == 2) {
break;
}
else {
p += 2;
}
}
return p;
}
/// <summary>
/// 創建哈希表
/// </summary>
/// <param name="table_size">表長度</param>
/// <returns>表頭指針</returns>
HashTable* CreateTable(int table_size) {
HashTable* h = (HashTable*)malloc(sizeof(HashTable));
h->tablesize = NextPrime(table_size);
h->heads = (LNode*)malloc(h->tablesize * sizeof(LNode));
for (int i = 0; i < h->tablesize; i++) {
h->heads[i].next = nullptr;
}
return h;
}
/// <summary>
/// 散列函數
/// </summary>
/// <param name="key">鍵</param>
/// <param name="n">鏈表長度</param>
/// <returns>鍵值</returns>
int Hash(int key, int n) {
return key % n;
}
/// <summary>
/// 查找元素
/// </summary>
/// <param name="h">表頭指針</param>
/// <param name="key">要找的鍵</param>
/// <returns>找到元素的指針</returns>
LNode* Find(HashTable* h, int key) {
int pos = Hash(key, h->tablesize);
LNode* p = h->heads[pos].next;
while (p && key != p->data) {
p = p->next;
}
return p;
}
/// <summary>
/// 插入新元素
/// </summary>
/// <param name="h">表頭</param>
/// <param name="key">插入的鍵</param>
/// <returns>是否完成</returns>
bool Insert(HashTable* h, int key) {
LNode* p = Find(h, key);
if (!p) {
LNode* newNode = (LNode*)malloc(sizeof(LNode));
newNode->data = key;
int pos = Hash(key, h->tablesize);
newNode->next = h->heads[pos].next;
h->heads[pos].next = newNode;
return true;
}
else {
return false;
}
}
/// <summary>
/// 移除元素
/// </summary>
/// <param name="h">表頭</param>
/// <param name="key">要刪除的鍵</param>
/// <returns>是否完成</returns>
bool Remove(HashTable* h, int key) {
LNode* p0 = Find(h, key);
if (p0) {
int pos = Hash(key, h->tablesize);
LNode* p = h->heads[pos].next;
if (p == p0) {
h->heads[pos].next = p->next;
p->next = nullptr;
free(p);
return true;
}
while (p && p->next != p0) {
p = p->next;
}
p->next = p0->next;
p0->next = nullptr;
free(p0);
return true;
}
else {
return false;
}
}
/// <summary>
/// 銷毀鏈表
/// </summary>
/// <param name="h">表頭元素</param>
void DestroyTable(HashTable* h) {
LNode* p, * temp;
for (int i = 0; i < h->tablesize; i++) {
p = h->heads[i].next;
while (p) {
temp = p->next;
free(p);
p = temp;
}
}
free(h->heads);
free(h);
}
代碼有參考。上面就實現了一個基於鏈表的哈希表,解決哈希沖突的方法是線性探測,直接加一。
STL中使用unordered_map
實現哈希表。
template <class Key, class T, class Hash = hash
, class Pred = equal_to , class Alloc = allocator<pair<const Key, T>>> class unordered_map;
關於hash_map與unordered_map:
由於在C++標准庫中沒有定義散列表hash_map,標准庫的不同實現者將提供一個通常名為hash_map的非標准散列表。因為這些實現不是遵循標准編寫的,所以它們在功能和性能保證上都有微妙的差別。
從C++11開始,哈希表實現已添加到C++標准庫標准。決定對類使用備用名稱,以防止與這些非標准實現的沖突,並防止在其代碼中有hash_table的開發人員無意中使用新類。
所選擇的備用名稱是unordered_map,它更具描述性,因為它暗示了類的映射接口和其元素的無序性質。
Key
就是鍵的類型,T
就是值的類型,Hash
就是散列函數,Pred
是一個二元謂詞,用來判斷鍵是否已經存在的,Alloc
是分配器。散列函數、謂詞和分配器都用默認的就好,我們一般只用定義鍵和值的類型。需要強調的是哈希表是無序容器,迭代器之間不能用大於小於號比較,可以與隊和棧做對比。
一些api也是百度去看吧,做一道簡單LeetCode示范一下:
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
if (nums.size() < 2) {
return {};
}
unordered_map<int, int> hashtable;
for (int i = 0; i < nums.size(); i++) {
if (hashtable.find(nums[i]) != hashtable.end()) {
return { hashtable[nums[i]],i };
}
hashtable[target - nums[i]] = i;
}
return {};
}
};
哈希去重還是很好用的,看到題目說沒有重復就多想想哈希。
二叉樹(Binary Tree)
二叉樹是一種非常重要的數據結構,許多實際問題抽象出來的數據結構都是二叉樹,或者說是樹。

不知道有沒有發現,之前的棧、隊、哈希表的數據元素前后關系都是一對一的,再看到二叉樹,它是一對多的,如果繼續往下學,圖的數據結構是多對多的,所以二叉樹開始就是非線性結構的數據結構了。
基本術語和性質
先介紹一下二叉樹相關的術語:二叉樹頂端稱為根節點,二叉樹的節點包含一個數據元素及指向其左、右子樹的兩個分支,分別成為左分支和右分支。節點的左、右子樹的根稱為該節點的左、右孩子,統稱為孩子,該節點稱為孩子的雙親。同一個孩子之間可互稱為兄弟。節點的孩子個數稱為節點的度,度為0的節點稱為葉子節點,非葉子節點的結構稱為內部結構或分支節點。節點的層次從根節點開始定義,根為第1層,根的孩子為第2層,如此類推。二叉樹中節點的最大層次稱為二叉樹的深度或高度。一棵深度為k且有 2^k-1個節點的二叉樹稱為滿二叉樹。一棵深度為k的有n個結點的二叉樹,對樹中的結點按從上至下、從左到右的順序進行編號,如果編號為i(1≤i≤n)的結點與滿二叉樹中編號為i的結點在二叉樹中的位置相同,則這棵二叉樹稱為完全二叉樹。
應該看暈了吧,下面上點圖:
- 層的定義
- 度的定義
- 滿二叉樹的定義
- 完全二叉樹的定義
最后再補充一下二叉樹的性質,這些性質對后面的使用會很有幫助:

存儲與遍歷
上面那些只不過是二叉樹的術語,下面才是這個數據結構的使用。
在存儲二叉樹時,除了存儲它的每個節點數據以外,還要表示節點之間的一對多邏輯,也就是父子關系。根據二叉樹的性質5,我們可以用一維數組去存儲完全二叉樹,每個節點的雙親和孩子都可以根據性質5計算得出。

但是一般的二叉樹就不太方便用一維數組存了,因此我們可以定義自己的數據結構。
template<typename T>
struct TreeNode {
T data;
TreeNode<T>* leftChild, * rightChild;
TreeNode() {}
TreeNode(T x) : data(x), leftChild(nullptr), rightChild(nullptr) {}
};
template<typename T>
class BinaryTree {
private:
void DeleteNode(TreeNode<T>* head);
public:
TreeNode<T>* root;
BinaryTree() :root(nullptr) {};
~BinaryTree();
void Insert(const T& x, TreeNode<T>*& head);
void PreTraversal(TreeNode<T>* head); // 前序遍歷
void InTraversal(TreeNode<T>* head); // 中序遍歷
void PostTraversal(TreeNode<T>* head); // 后序遍歷
};
template<typename T>
void BinaryTree<T>::DeleteNode(TreeNode<T>* head) {
if (head != nullptr) {
DeleteNode(head->leftChild);
DeleteNode(head->rightChild);
delete head;
}
}
template<typename T>
BinaryTree<T>::~BinaryTree() {
DeleteNode(root);
}
template<typename T>
void BinaryTree<T>::Insert(const T& x, TreeNode<T>*& head) {
if (head == nullptr) {
head = new TreeNode<T>(x);
return;
}
std::queue<TreeNode<T>*> level;
TreeNode<T>* temp;
level.push(head);
while (!level.empty()) {
temp = level.front();
level.pop();
if (temp->leftChild) {
level.push(temp->leftChild);
}
else {
Insert(x, temp->leftChild);
break;
}
if (temp->rightChild) {
level.push(temp->rightChild);
}
else {
Insert(x, temp->rightChild);
break;
}
}
}
template<typename T>
void BinaryTree<T>::PreTraversal(TreeNode<T>* head) {
if (head != nullptr) {
std::cout << head->data << " ";
PreTraversal(head->leftChild);
PreTraversal(head->rightChild);
}
}
template<typename T>
void BinaryTree<T>::InTraversal(TreeNode<T>* head) {
if (head != nullptr) {
InTraversal(head->leftChild);
std::cout << head->data << " ";
InTraversal(head->rightChild);
}
}
template<typename T>
void BinaryTree<T>::PostTraversal(TreeNode<T>* head) {
if (head != nullptr) {
PostTraversal(head->leftChild);
PostTraversal(head->rightChild);
std::cout << head->data << " ";
}
}
上面是一個簡單的二叉樹。先定義一個struct
,把一個結點該有的東西寫進去,要注意的是,我的定義里面並沒有包含指向父結點的指針,只有指向左右孩子的指針,這種是雙叉鏈表。某些情況下可能需要頻繁調用父結點的數據,這個時候你可以寫一個包含指向父結點的指針的結構體,變成一個三叉鏈表。
接下來就是一些常規的函數,插入啦、查找啦還有重要的三種遍歷。 下面解釋遍歷的時候都會用這個二叉樹做說明:

前序遍歷是先訪問根結點,再訪問左子樹,最后訪問右子樹。像上面這個二叉樹,它最后會輸出“1 2 4 5 3 6 7 ”,稍微看一下前序遍歷的代碼就看的出來。中序遍歷則是先訪問左子樹,再訪問根結點,最后訪問右子樹。同樣是上面的二叉樹,最后輸出結果為“4 2 5 1 6 7 3 ”。后序遍歷就是先訪問左子樹,再訪問右子樹,最后訪問根結點,輸出的結果為“4 5 2 6 7 3 1 ”。其實再認真看看三種遍歷的算法,其實差別真的很小,就是cout
放的位置不一樣而已,但是這就意味着輸出的時機不一樣,三種遍歷方法記起來也很容易。
其實除了這三種遍歷,還有一種遍歷叫做層次遍歷,它是按二叉樹的層次從小到大且每層從左到右的順序一次訪問結點,如果遍歷上圖的二叉樹,輸出的結果就是“1 2 3 4 5 6 7 ”。這種遍歷我把它用在了插入的代碼上,為了把二叉樹插成完全二叉樹,用層次遍歷的方法按順序插滿。為了檢驗是否為完全二叉樹,我們可以稍微測試一下:

很顯然,跟上面說的結果一致,證明它確實是完全二叉樹。
最后目光聚焦刪除二叉樹的代碼,它其實就是用了后序遍歷,不斷的刪除結點,最終把根結點刪掉。關於遍歷的應用還很多,留給讀者自己摸索了。
堆(Heap)
堆是一類完全二叉樹,它可以用於高效地實現排序、選擇最大(小)值和優先隊列。下面先聊一聊基本的定義和術語。
堆有兩類,一類叫小頂堆(小根堆),一類叫大頂堆(大根堆)。若堆中所有非葉子結點均不大於其左右孩子結點,則稱為小頂堆(小根堆)。大頂堆(大根堆)的定義可以讀者自己推導一下。

堆中根結點的位置稱為堆頂,最后結點的位置稱為堆尾,結點的個數稱之為堆長度。 堆很實用,因為它在每次添加或修改元素時,都可以自動調整自身的結構,使其重新成為一個大根堆或者小根堆。而且這個過程只要付出logN的代價(樹的高度),在大樣本的情況下優勢非常大。
平時用堆的話一般就是用優先隊列了,STL中有priority_queue
容器去實現。
template <class T, class Container = vector
,
class Compare = less> class priority_queue;
第一個參數是數據的類型,第二個參數是實現優先隊列底層容器的類型,默認是vector
,第三個參數是比較數據的方法,這個比較方法一般是重載了operator()
的類,實現兩個數據的比較。less
實現的是大根堆,greater
實現的是小根堆。如果元素的類型不是基本類型,那第三個參數一般需要自己重寫一個比較函數的,不然less
和greater
不知道怎么去比較。
普通的隊列是一種先進先出的數據結構,元素在隊列尾追加,而從隊列頭刪除。在優先隊列中,元素被賦予優先級。當訪問元素時,具有最高優先級的元素最先刪除。優先隊列具有最高級先出 (First In Largest Out)的行為特征。
優先隊列也是隊,實現它的底層容器是vector
,所以vector
有的一些函數它都會有。
做一道LeetCode來介紹堆的優點:
class MedianFinder {
priority_queue<int, vector<int>, less<int>> queMin;
priority_queue<int, vector<int>, greater<int>> queMax;
public:
MedianFinder() {}
void addNum(int num) {
if (queMin.empty() || num <= queMin.top()) {
queMin.push(num);
if (queMax.size() + 1 < queMin.size()) {
queMax.push(queMin.top());
queMin.pop();
}
}
else {
queMax.push(num);
if (queMax.size() > queMin.size()) {
queMin.push(queMax.top());
queMax.pop();
}
}
}
double findMedian() {
if (queMin.size() > queMax.size()) {
return queMin.top();
}
return (queMin.top() + queMax.top()) / 2.0;
}
};
這道題的題解很有趣,建了一個大根堆和一個小根堆,相當於兩個金字塔塔尖相碰,中間的那個數就是中位數。
二叉查找樹(Binary Search Tree)
二叉樹的查找一般是通過遍歷去實現,效率較低,因為元素的順序沒有規律。二叉查找樹就是一種專門為查找而設計的樹形數據結構,可以大大提高查找效率。
二叉查找樹的定義:
若左子樹非空,則左子樹上所有的結點的值均小於根節點的值;
若右子樹非空,則右子樹上所有的結點的值均大於根結點的值;
左、右子樹也分別是二叉查找樹
其實二叉查找樹的定義跟堆的定義挺像的,建議將兩者比較着去記。由二叉查找樹的定義可以得知,結點的值不允許重復,它的中序遍歷是有序的。
下面實現自己定義的二叉查找樹:
template<typename T>
struct BSTreeNode {
T data;
BSTreeNode<T>* leftChild, * rightChild;
BSTreeNode() :data(NULL), leftChild(nullptr), rightChild(nullptr) {}
BSTreeNode(T x) :data(x), leftChild(nullptr), rightChild(nullptr) {}
};
template<typename T>
class BinarySearchTree {
void DeleteTree(BSTreeNode<T>* head);
public:
BSTreeNode<T>* root;
BinarySearchTree() :root(nullptr) {}
BinarySearchTree(BSTreeNode<T>* head) :root(head) {}
~BinarySearchTree();
BSTreeNode<T>* BSTSearch(const T& target);
void BSTInsert(const T& nodedata);
void BSTRemove(const T& target);
void InOrderPrint(BSTreeNode<T>* head);
};
template<typename T>
void BinarySearchTree<T>::DeleteTree(BSTreeNode<T>* head) {
if (head != nullptr) {
DeleteTree(head->leftChild);
DeleteTree(head->rightChild);
delete head;
}
}
template<typename T>
BinarySearchTree<T>::~BinarySearchTree() {
DeleteTree(root);
}
template<typename T>
BSTreeNode<T>* BinarySearchTree<T>::BSTSearch(const T& target) {
BSTreeNode<T>* node = root;
while (node != nullptr) {
if (target < node->data) {
node = node->leftChild;
}
else if (target > node->data) {
node = node->rightChild;
}
else {
break;
}
}
return node;
}
template<typename T>
void BinarySearchTree<T>::BSTInsert(const T& nodedata) {
if (!root) {
root = new BSTreeNode<T>(nodedata);
return;
}
else {
BSTreeNode<T>* temp = nullptr;
BSTreeNode<T>* current = root;
while (current) {
if (nodedata < current->data) {
temp = current;
current = current->leftChild;
continue;
}
else if (nodedata > current->data) {
temp = current;
current = current->rightChild;
continue;
}
else {
return;
}
}
current = new BSTreeNode<T>;
current->data = nodedata;
current->leftChild = nullptr;
current->rightChild = nullptr;
if (temp->data > current->data) {
temp->leftChild = current;
}
else {
temp->rightChild = current;
}
return;
}
}
template<typename T>
void BinarySearchTree<T>::BSTRemove(const T& target) {
BSTreeNode<T>* pre = BSTSearch(target);
if (!pre) {
std::cout << "DON'T EXIST" << std::endl;
return;
}
BSTreeNode<T>* deletenodeParent = nullptr;
BSTreeNode<T>* deletenodeChild = nullptr;
if (!root) {
return;
}
else {
BSTreeNode<T>* tempParent = nullptr;
BSTreeNode<T>* tempChild = root;
while (tempChild) {
if (target < tempChild->data) {
tempParent = tempChild;
tempChild = tempChild->leftChild;
continue;
}
else if (target > tempChild->data) {
tempParent = tempChild;
tempChild = tempChild->rightChild;
continue;
}
else {
deletenodeParent = tempParent;
deletenodeChild = tempChild;
break;
}
}
if (deletenodeChild->leftChild == nullptr && deletenodeChild->rightChild == nullptr) {
delete deletenodeChild;
}
else if (deletenodeChild->leftChild != nullptr && deletenodeChild->rightChild == nullptr) {
if (deletenodeParent->leftChild == deletenodeChild) {
deletenodeParent->leftChild = deletenodeChild->leftChild;
delete deletenodeChild;
}
else {
deletenodeParent->rightChild = deletenodeChild->leftChild;
delete deletenodeChild;
}
}
else if (deletenodeChild->leftChild == nullptr && deletenodeChild->rightChild != nullptr) {
if (deletenodeParent->leftChild == deletenodeChild) {
deletenodeParent->leftChild = deletenodeChild->rightChild;
delete deletenodeChild;
}
else {
deletenodeParent->rightChild = deletenodeChild->rightChild;
delete deletenodeChild;
}
}
else {
BSTreeNode<T>* temp = deletenodeChild->rightChild;
while (temp->rightChild) {
temp = temp->rightChild;
}
if (deletenodeParent->leftChild == deletenodeChild) {
deletenodeParent->leftChild = temp;
temp->rightChild = deletenodeChild->rightChild;
temp->leftChild = deletenodeChild->leftChild;
delete deletenodeChild;
}
else {
deletenodeParent->rightChild = temp;
temp->leftChild = deletenodeChild->leftChild;
temp->rightChild = deletenodeChild->rightChild;
delete deletenodeChild;
}
}
}
}
template<typename T>
void BinarySearchTree<T>::InOrderPrint(BSTreeNode<T>* head) {
if (head != nullptr) {
InOrderPrint(head->leftChild);
std::cout << head->data << " ";
InOrderPrint(head->rightChild);
}
}
實現了一些基本的功能:查找、插入、移除、中序遍歷,最后測試一下成果:

可以看出來中序遍歷就是升序排序之后再輸出的結果,這點非常好用。
另外需要注意的是移除操作,如果只是單純刪除目標結點,有可能導致刪除后的二叉樹不滿足查找樹的性質,所以為了解決這個問題,有兩種做法:
- 在目標結點的左子樹中找到最大值所在的結點補到被移除結點處
- 在目標結點的右子樹中找到最小值所在的結點補到被移除結點處
兩種方法都能保證刪除后的二叉樹依然滿足查找樹的性質,我用了第一種方法,就是在左子樹中找最右的結點。還有一點要指出,在實現插入、移除功能的時候,頻繁使用了父節點的數據,所以如果希望代碼好看點,可以把樹的結點設計成帶父節點指針的三叉鏈表,方便讀取父節點的信息。
平衡二叉樹(Balanced Binary Sort Tree)
在寫完查找樹之后可以思考一下,怎樣的查找樹查找性能最優?答案是:樹的高度應該為最小。滿二叉樹和完全二叉樹的高度都是最小的,但是要在插入或者刪除結點之后維持其高度最小的代價較大。平衡二叉查找樹是一種可以兼顧查找和維護性能的折中方案。
平衡二叉查找樹,簡稱平衡二叉樹,它是具有以下性質的二叉樹:
- 左子樹和右子樹的高度之差絕對值不超過1
- 它的左子樹和右子樹都是平衡二叉樹
定義的第二條需要注意,平衡二叉樹本身是二叉查找樹。我們把左子樹和右子樹的高度之差定義為平衡因子(Balance Factor),按照平衡二叉樹的定義,每個結點的平衡因子只可能為-1、0、1,只要有一個結點的平衡因子的絕對值大於1,那么這棵樹就失去了平衡。
在平衡二叉樹中插入一個新的結點后,從該結點起向上尋找第一個不平衡的結點(平衡因子變成了2或-2),以確定該樹是否失衡。如果找到了,則以該結點為根的子樹稱為最小失衡樹。如下圖,在插入了49之后,以55為根結點的子樹即為最小失衡樹。

我們根據插入結點的位置,把最小失衡樹分了四類:
- LL型:在最小失衡樹的左孩子的左子樹上插入結點
- RR型:在最小失衡樹的右孩子的右子樹上插入結點
- LR型:在最小失衡樹的左孩子的右子樹上插入結點
- RL型:在最小失衡樹的右孩子的左子樹上插入結點
對於簡單的LL和RR型只需要一次右旋和左旋就能恢復平衡:
右旋是以最小失衡樹的左孩子為軸(A結點),對根節點進行右旋(B結點),同時A結點的右子樹會成為B結點的左子樹。
左旋就是跟右旋相反的操作。它是以B結點為軸進行旋轉,B結點的左子樹會成為A結點的右子樹。
簡單的LL和RR型很好解決(其實也不是很好解決= =),那對於復雜一點的LR和RL型其實就是需要兩次旋轉。對於LR型,可以先進行左旋,再進行右旋;對於RL型,則先進行右旋再進行左旋。實在找不到好的動圖,大家自己畫圖理解一下吧。(摸了)
事實上平衡二叉樹有很多種,最出名的是由前蘇聯數學家Adelse-Velskil和Landis在1962年提出的高度平衡二叉樹,根據提出者的英文名字首寫字母簡稱為AVL樹。接下來我們實現一下平衡二叉樹:
template <typename T>
struct AVLTreeNode {
T data;
int height;
AVLTreeNode<T>* leftChild;
AVLTreeNode<T>* rightChild;
AVLTreeNode<T>(const T& theData) : data(theData), leftChild(nullptr), rightChild(nullptr), height(0) {}
};
template <typename T>
class AVLTree {
public:
AVLTree<T>() :root(nullptr) {}
~AVLTree<T>() {}
void Insert(T nodedata);
void DeleteNode(T nodedata);
void Search(T& nodedata);
void InorderPrinter();
private:
AVLTreeNode<T>* root;
void Add(AVLTreeNode<T>*& t, T& x); //插入的內部接口
bool Delete(AVLTreeNode<T>*& t, T& x); //刪除的內部接口
bool Find(AVLTreeNode<T>* t, const T& x) const; //查找的內部接口
void InorderTraversal(AVLTreeNode<T>* t); //中序遍歷的內部接口
AVLTreeNode<T>* FindMin(AVLTreeNode<T>* t) const; //查找最小值結點
AVLTreeNode<T>* FindMax(AVLTreeNode<T>* t) const; //查找最大值結點
int GetHeight(AVLTreeNode<T>* t); //求樹的高度
AVLTreeNode<T>* LL_Rotate(AVLTreeNode<T>* t); //左旋
AVLTreeNode<T>* RR_Rotate(AVLTreeNode<T>* t); //右旋
AVLTreeNode<T>* LR_Rotate(AVLTreeNode<T>* t); //雙旋,右左
AVLTreeNode<T>* RL_Rotate(AVLTreeNode<T>* t); //雙旋,左右
};
template<typename T>
void AVLTree<T>::Insert(T nodedata) {
Add(root, nodedata);
}
template<typename T>
void AVLTree<T>::DeleteNode(T nodedata) {
Delete(root, nodedata);
}
template<typename T>
void AVLTree<T>::Search(T& nodedata) {
Find(root, nodedata);
}
template<typename T>
void AVLTree<T>::InorderPrinter() {
InorderTraversal(root);
}
template <typename T>
void AVLTree<T>::Add(AVLTreeNode<T>*& t, T& x) {
if (!t) {
t = new AVLTreeNode<T>(x);
}
else if (x < t->data) {
Add(t->leftChild, x);
if (GetHeight(t->leftChild) - GetHeight(t->rightChild) > 1) {
if (x < t->leftChild->data) {
t = LL_Rotate(t);
}
else {
t = LR_Rotate(t);
}
}
}
else if (x > t->data) {
Add(t->rightChild, x);
if (GetHeight(t->rightChild) - GetHeight(t->leftChild) > 1) {
if (x > t->rightChild->data) {
t = RR_Rotate(t);
}
else {
t = RL_Rotate(t);
}
}
}
else {
}
t->height = std::max(GetHeight(t->leftChild), GetHeight(t->rightChild)) + 1;
}
template <typename T>
bool AVLTree<T>::Delete(AVLTreeNode<T>*& t, T& x) {
if (t == nullptr) {
return false;
}
else if (t->data == x) {
if (t->leftChild != nullptr && t->rightChild != nullptr) {
if (GetHeight(t->leftChild) > GetHeight(t->rightChild)) {
t->data = FindMax(t->leftChild)->data;
Delete(t->leftChild, t->data);
}
else {
t->data = FindMin(t->rightChild)->data;
Delete(t->rightChild, t->data);
}
}
else {
AVLTreeNode<T>* old = t;
t = t->leftChild ? t->leftChild : t->rightChild;
delete old;
}
}
else if (x < t->data) {
Delete(t->leftChild, x);
if (GetHeight(t->rightChild) - GetHeight(t->leftChild) > 1) {
if (GetHeight(t->rightChild->leftChild) > GetHeight(t->rightChild->rightChild)) {
t = RL_Rotate(t);
}
else {
t = RR_Rotate(t);
}
}
else {
t->height = std::max(GetHeight(t->leftChild), GetHeight(t->rightChild)) + 1;
}
}
else {
Delete(t->rightChild, x);
if (GetHeight(t->leftChild) - GetHeight(t->rightChild) > 1) {
if (GetHeight(t->leftChild->rightChild) > GetHeight(t->leftChild->leftChild)) {
t = LR_Rotate(t);
}
else {
t = LL_Rotate(t);
}
}
else {
t->height = std::max(GetHeight(t->leftChild), GetHeight(t->rightChild)) + 1;
}
}
return true;
}
template <typename T>
bool AVLTree<T>::Find(AVLTreeNode<T>* t, const T& x) const {
if (t == nullptr) {
return false;
}
if (x < t->data) {
return Find(t->leftChild, x);
}
else if (x > t->data) {
return Find(t->rightChild, x);
}
else {
return true;
}
}
template <typename T>
void AVLTree<T>::InorderTraversal(AVLTreeNode<T>* t) {
if (t) {
InorderTraversal(t->leftChild);
std::cout << t->data << ' ';
InorderTraversal(t->rightChild);
}
}
template <typename T>
AVLTreeNode<T>* AVLTree<T>::FindMax(AVLTreeNode<T>* t) const {
if (t == nullptr) {
return nullptr;
}
if (t->rightChild == nullptr) {
return t;
}
return FindMax(t->rightChild);
}
template <typename T>
AVLTreeNode<T>* AVLTree<T>::FindMin(AVLTreeNode<T>* t) const {
if (t == nullptr) {
return nullptr;
}
if (t->leftChild == nullptr) {
return t;
}
return FindMin(t->leftChild);
}
template <typename T>
int AVLTree<T>::GetHeight(AVLTreeNode<T>* t) {
if (t == nullptr) {
return -1;
}
else {
return t->height;
}
}
template <typename T>
AVLTreeNode<T>* AVLTree<T>::LL_Rotate(AVLTreeNode<T>* t) {
AVLTreeNode<T>* q = t->leftChild;
t->leftChild = q->rightChild;
q->rightChild = t;
t = q;
t->height = std::max(GetHeight(t->leftChild), GetHeight(t->rightChild)) + 1;
q->height = std::max(GetHeight(q->leftChild), GetHeight(q->rightChild)) + 1;
return q;
}
template <typename T>
AVLTreeNode<T>* AVLTree<T>::RR_Rotate(AVLTreeNode<T>* t) {
AVLTreeNode<T>* q = t->rightChild;
t->rightChild = q->leftChild;
q->leftChild = t;
t = q;
t->height = std::max(GetHeight(t->leftChild), GetHeight(t->rightChild)) + 1;
q->height = std::max(GetHeight(q->leftChild), GetHeight(q->rightChild)) + 1;
return q;
}
template <typename T>
AVLTreeNode<T>* AVLTree<T>::LR_Rotate(AVLTreeNode<T>* t) {
AVLTreeNode<int>* q = RR_Rotate(t->leftChild);
t->leftChild = q;
return LL_Rotate(t);
}
template <typename T>
AVLTreeNode<T>* AVLTree<T>::RL_Rotate(AVLTreeNode<T>* t) {
AVLTreeNode<int>* q = RR_Rotate(t->rightChild);
t->rightChild = q;
return RR_Rotate(t);
}
上面簡單實現了一下AVL樹的一些基本操作,旋轉和刪除結點操作是我個人認為最難實現的部分,建議畫圖去輔助理解,同時還能對比查找樹的做法,因為平衡樹也是一種查找樹,他們二者肯定是有很多相似之處的。
最后寫一個例子測試一下:

中序遍歷還是有序的輸出,符合我們的要求。
圖(Graph)
圖形結構是一種比樹形結構更復雜的非線性數據結構,而且概念也特別多,下面一點點地來了解它。
基本術語
在圖中,將數據元素稱為頂點(Vertex),頂點之間的關系稱為邊(Edge),圖由有限頂點集V和有限邊集E組成,記為:G=(V, E),其中頂點總數記為n,邊的總數記為e。
為了解釋方便,用一張圖來形容有向圖和無向圖:

有向圖的邊是帶方向的,只能按照箭頭的方向去訪問其他頂點。有向圖的邊稱為有向邊,或者弧(Arc)。無向圖的邊則沒有方向性,稱為無向邊,簡稱邊,兩頂點間可以相互訪問。
假設兩個圖G=(V, E)和G'=(V', E'),如果V'⊆V且E'⊆E,則稱G'為G的子圖。說人話就是在G中切割一部分出來,不改變邊的方向,那切割的那些部分都是G的子圖。
包含所有可能的邊的圖稱為完全圖。無向完全圖包含n (n - 1) / 2條邊,有向完全圖包含n (n - 1)條弧。說人話就是把所有頂點之間都牽條線,無向圖就變無向完全圖了。對於有向圖就要考慮方向性,除了“過去”還要“回來”,所以每個頂點之間牽兩條線,就這么簡單。
在圖中,頂點的度(Degree)是指依附於該頂點的邊數。無向圖中就看頂點有多少條邊就行了。對於有向圖,還要區分入度(InDegree)和出度(OutDegree),入度是指該頂點為終點的弧的數目,出度是指以該頂點為起點的弧的數目,有向圖頂點的度是出度和入度之和。
有時候,邊或弧需要附加一些屬性信息,比如兩個頂點之間的距離、旅行時間或者某種代價等,通常稱此信息為權(Weight),帶權的圖又稱為帶權圖,或簡稱為網(Network)。
如果從頂點A₁到頂點Aₙ的邊(弧)都存在,則稱存在從頂點A₁到頂點Aₙ的長度為n - 1的路徑。如果路徑上頂點都不同,則稱這個路徑為簡單路徑。路徑長度是指路徑包含的邊數。如果一條路徑從某個頂點出發,最終連接到它自身,則稱此路徑為回路。
在無向圖中,如果頂點A₁到頂點Aₙ有路徑,則稱A₁和Aₙ是連通的。如果圖中任意兩個頂點都是連通的,則稱此圖為連通圖(Connected Graph)。在有向圖中,如果頂點A₁到頂點Aₙ有路徑,頂點Aₙ到頂點A₁也有路徑,則稱A₁和Aₙ是連通的。如果圖中任意兩個頂點都是連通的,則稱此圖為強連通圖(Strong Connected Graph)。
連通圖的生成樹(Spanning Tree)是含有所有頂點且只有n - 1條邊的連通子圖。首先,它包含了所有的頂點;齊次,它只有n - 1條邊;最后,它是子圖。這三點很重要,不要看到有n - 1條邊就說這是生產樹。
鄰接矩陣和鄰接表
圖這么復雜,我們要怎么去存它呢?基於多對多這一性質,我們可以設計一種叫鄰接矩陣(Adjacency Matrix)和鄰接表(Adjacency List)的結構去儲存它,下面是兩種結構的示意圖:

兩種結構都有一個頂點數組,用來存放頂點的一些信息,比如名字、順序等。鄰接矩陣的關系數組是存邊的信息,它是一個二維數組,如果存在頂點V[i]到V[j]的邊,關系數組的A[i][j]就等於1,如果沒有,就等於0,對於網還可以存權。容易得出,左上到右下的對角線自然全是0,因為自己不用跟自己連線。而鄰接表就更直觀了,用了單鏈表的方法,把相鄰的頂點接在一起。對於有向圖和無向圖,兩者的實現上有是區別的,這也導致兩者在性能上有所不同。當圖的邊數很少時(即稀疏圖),關系數組含有大量0,浪費了大量內存。在存儲稀疏圖的時候會更常用鄰接表。
在設計上,鄰接矩陣本質上就是矩陣,所以可以用一個二維數組去存。對於鄰接表,本質上是若干鏈表組成的,所以可以用一個數組去存每個鏈表的頭節點。
圖的遍歷
圖的遍歷有兩種:廣度優先遍歷和深度優先遍歷,又叫廣度優先搜索(BFS)和深度優先搜索(DFS)。其實對於深度優先遍歷,我們在二叉樹那一塊用的比較多了,前、中、后序遍歷的思想都是深度優先遍歷:一條路走到底。對於廣度優先遍歷,其實二叉樹的層次遍歷就是這樣的思想:把鄰接的、尚未訪問過的節點都訪問一遍。對於兩種遍歷我通過兩道LeetCode來說明。
深度優先搜索
首先是深度優先搜索:
class Solution {
public:
vector<vector<int>> result;
vector<int> temp;
void dfs(vector<vector<int>>& graph, int x, int n) {
if (x == n) {
result.push_back(temp);
return;
}
for (auto& y : graph[x]) {
temp.push_back(y);
dfs(graph, y, n);
temp.pop_back();
}
}
vector<vector<int>> allPathsSourceTarget(vector<vector<int>>& graph) {
temp.push_back(0);
dfs(graph, 0, graph.size() - 1);
return result;
}
};
題目中的有向無環圖(Directed Acyclic Graph)是指不存在回路的有向圖。對於搜索路徑,我們可以從起點出發,順着任意路徑探索到達終點的路徑,如果找到了,就將路徑保存到數組中。由於這是一個有向無環圖,所以不用擔心會原地轉圈。這里的搜索路徑是沿着一條路走到底的,符合DFS的思想。
廣度優先搜索
同意是上面的題目,我也可以用廣度優先搜索:
class Solution {
public:
vector<vector<int>> allPathsSourceTarget(vector<vector<int>>& graph) {
queue<vector<int>> que;
que.push({0});
while (!que.empty()) {
auto path = que.front();
que.pop();
auto cur = path.back();
for (auto& next : graph[cur]) {
path.push_back(next);
if (next == (int)graph.size() - 1) {
ans.push_back(path);
}
else {
que.push(path);
}
path.pop_back();
}
}
return ans;
}
private:
vector<vector<int>> ans;
};
我用的是DFS,BFS是在題解里面找的。道理也很簡單,用一個隊列存圖,然后暴力遍歷,直到找到目的地。
結語
斷斷續續寫了兩個月,有懶的原因,也有學習上的原因。這學期的課超多作業,每周都要寫40道大題,傳熱學和工程熱力學簡直要我命= =。另外騰訊的UE公開課也占用了我不少時間,最近也開始學UE了,時間完全不夠用。我知道最后的圖論介紹太少了,一些經典算法沒有展示。但是我想這只是對數據結構的基本介紹,感興趣的可以自己的往深學,比如我現在還沒寫出來的Dijkstra算法。另外平衡樹之前的代碼有點臭,但是有點懶得改了。總之,這篇博客暫時先這樣了,后面可能會慢慢更新,再說吧。