菜雞數據結構


數據結構復習

一個月學完數據結構,寫一篇究極總結,當復習了

棧(Stack)

回歸原始,可以用鏈表實現棧,或者說所有的數據結構都可以用鏈表實現。STL中用stack容器實現棧。棧的特征就是先進后出First In Last Out。

template <class T, class Container = deque > class stack;

棧的示意圖

T就是棧所存的元素類型,container是容器適配器,不寫第二個參數的情況下默認是deque,你也可以自己把它換成vector,具體搜索容器適配器,這里不多贅述。
需要注意的一點是,stack沒有迭代器,不允許遍歷,只能在棧頂進行pushtoppop操作。

常用函數

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;

下面分別用pushemplace插入元素

	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實現的是小根堆。如果元素的類型不是基本類型,那第三個參數一般需要自己重寫一個比較函數的,不然lessgreater不知道怎么去比較。
普通的隊列是一種先進先出的數據結構,元素在隊列尾追加,而從隊列頭刪除。在優先隊列中,元素被賦予優先級。當訪問元素時,具有最高優先級的元素最先刪除。優先隊列具有最高級先出 (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算法。另外平衡樹之前的代碼有點臭,但是有點懶得改了。總之,這篇博客暫時先這樣了,后面可能會慢慢更新,再說吧。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM