1. 程式人生 > >大小堆的實現(C++)

大小堆的實現(C++)

過了一段時間了,還是記錄一下,最近感覺記憶力下降的很快啊。

大小堆的構造

首先得知道大小堆是個什麼,大小堆是一種二叉樹,它的要求比起平衡樹來說是簡單一些的,只有一個,就是所有父節點必須大於自己的子節點,但是沒有什麼左孩子必須小於右孩子這種。到這裡其實大小堆的作用很容易看出,最大堆就是根節點最大,最小堆就是根節點最小,可以用於排序。

作為測試,這裡使用整形陣列作為初始化資料,二叉樹可以直接使用c++單迴圈實現二叉樹的前,中,後序,層次遍歷中的結構,現在我們的任務是將一個無序的陣列轉為大小堆。如何做呢,首先我們將這個陣列先按層次遍歷順序對映到一棵二叉樹上,然後再進行排序。

層次遍歷這裡就不說了,前面也有,當有了一棵無序的二叉樹時,這裡有一個效率比較高的排序方式,這裡就把遍歷那篇的圖拿過來說明一下


這裡的數字我們既把它看做序號也把它看做節點值,很容易看出這是一個最小堆,我們現在要將其變為最大堆。我們很容易想到,肯定是需要將子節點與父節點進行比較,然後交換,但是如果是從根節點開始向下比較的話明顯是不明智的。因為其並不能知道底下哪個是最大,而二叉樹的特性是父子節點比較是容易的,兄弟比較是比較複雜的,而剛才的方案必定需要左右子樹最大值比較。所以這裡使用自底向上的方案,我們使用一個棧來存放所有父節點,剛好我們又是層次遍歷生成樹,所以出棧的順序正好是從最後一個父節點往根節點。

這裡的情況是4號節點第一個出棧,與左孩子比較,交換,4號節點此時為8,再與右孩子比較,交換,此時4號節點為9,結束。此時489這棵子樹已經是最大堆了,3號節點出棧,和剛才一樣,然後2號節點出棧,與左孩子比較,交換,這時4號節點為2,這時遞迴4號節點也就是左孩子,4號節點與其左孩子比較,交換,此時4號節點為4,4號節點再與右孩子比較,交換,此時4號節點值為8,最後2號節點與右孩子比較,不動,至此2號節點的子樹按層次遍歷為98524,這棵子樹現在也是最大堆了。然後當最後一個節點也就是根節點出棧,再執行一遍剛才的遞迴,整棵樹就是最大堆了。我們可以算算一共進行了幾次比較,大概是14次的樣子,這已經是很壞的情況了,所以我們可以認為時間複雜度為線性的,當然複雜度不是這麼分析的。

        enum SWAPMODE
	{
		MAX,
		MIN
	};

	//根據陣列建立堆,flag使用列舉值MIN或者MAX
	BinaryTreeNode<int> * InitHeap(std::vector<int> v, int flag);

	//交換兩個節點的值
	void Swap(BinaryTreeNode<int> *p1, BinaryTreeNode<int> *p2);

	//遞迴排序
	void Swap(BinaryTreeNode<int> *node, int flag);

	//插入節點
	void InsertHeap(BinaryTreeNode<int> *root, int data, int flag);

	//獲取指定節點的父節點指標
	BinaryTreeNode<int> * GetParent_heap(BinaryTreeNode<int> *root,BinaryTreeNode<int> *node);

	//獲取最後一個插入節點的父節點
	BinaryTreeNode<int> * GetLastParent(BinaryTreeNode<int> *root);

	//刪除堆的根節點並返回節點值
	int DeleteHeap(BinaryTreeNode<int> * root, int flag);

以上是標頭檔案定義。下面是構造堆

BinaryTreeNode<int>* InitHeap(std::vector<int> v, int flag)
{
	BinaryTreeNode<int> *p = new BinaryTreeNode<int>();	//
	BinaryTreeNode<int> *root = p;					//儲存根節點
	std::queue<BinaryTreeNode<int> *> q;				//定義佇列用於層次遍歷
	std::stack<BinaryTreeNode<int> *> s;				//定義棧用於儲存父節點
	q.push(p);
	int n = v.size();
	for (size_t i = 0; i < n; i++)
	{
		p = q.front();
		q.pop();
		p->data = v[i];
		if ((i + 1) * 2 <= n) {					//左孩子
			p->left = new BinaryTreeNode<int>();
			q.push(p->left);
			s.push(p);
			if ((i + 1) * 2 + 1 <= n) {			//右孩子
				p->right = new BinaryTreeNode<int>();
				q.push(p->right);
			}
		}
	}
	while (!s.empty())
	{
		BinaryTreeNode<int> *_p = s.top();			//依次彈出父節點進行遞迴排序
		s.pop();
		Swap(_p, flag);
	}
	return root;
}

void Swap(BinaryTreeNode<int>* p1, BinaryTreeNode<int>* p2)
{
	int temp;
	temp = p1->data;
	p1->data = p2->data;
	p2->data = temp;
}

void Swap(BinaryTreeNode<int>* node, int flag)
{
	if (node == nullptr) {
		return;
	}
	if (flag == MAX) {
		if (node->left != nullptr) {
			if (node->left->data > node->data) {
				Swap(node, node->left);
				Swap(node->left, flag);
			}
			if (node->right != nullptr) {
				if (node->right->data > node->data) {
					Swap(node, node->right);
					Swap(node->right, flag);
				}
			}
		}
	}
	else if (flag == MIN) {
		if (node->left != nullptr) {
			if (node->left->data < node->data) {
				Swap(node, node->left);
				Swap(node->left, flag);
			}
			if (node->right != nullptr) {
				if (node->right->data < node->data) {
					Swap(node, node->right);
					Swap(node->right, flag);
				}
			}
		}
	}
}

使用隨機陣列測試結果如下


這裡樹的顯示是另一塊的函式,程式碼比較多,因為是測試用的所以寫的比較凌亂,有機會可以單獨整理一下寫一下,主要是利用樹的層次遍歷獲取對映到陣列的下標,然後利用二叉樹的特性,最後一層為2^(k-1)個節點,然後就能向上推算出每層所佔輸出空間。

堆的插入

首先明確我們要做的事,插入一個節點,保持樹結構不被破壞,也就是所有父節點大於其子節點的原則。我們依然可以將其分為,首先插入一個節點到最後,然後遞迴排序其父節點。還是拿上面的圖來說,現在這棵樹已經是最小堆了,這時我們插入一個值為0的節點。首先我們將其插入到5號節點的左孩子位置,後面我們需要一個函式來專門尋找最後的一個空位,首先看最後一個父節點,如果父節點孩子滿了,就找序號最前的一個葉子節點,這裡5號就是。

插入後就是排序了,有了前面,這裡就是照葫蘆畫瓢了,直接遞迴排序父節點,所以,還需要一個函式來專門找指定節點的父節點,這裡我是直接返回父節點,如果做優化的話應該可以返回到根節點的父節點棧,這樣就不用每次都找了。

//堆的插入操作
void InsertHeap(BinaryTreeNode<int>* root, int data, int flag)
{
	BinaryTreeNode<int> *p = new BinaryTreeNode<int>();	//新節點
	p->data = data;
	BinaryTreeNode<int> *last = GetLastParent(root);	//獲取最後插入位置
	if (last != nullptr) {
		if (last->left != nullptr)			//如果左孩子不為空則插入否則插右孩子
			last->right = p;
		else
			last->left = p;
	}
	else
		throw std::exception("插入失敗");
	BinaryTreeNode<int> *pp = last;
	if (flag == MAX) {
		while (pp!=nullptr && pp->data<p->data)
		{
			Swap(pp, MAX);
			p = pp;
			pp = GetParent_heap(root, p);		//不斷向上排序
		}
	}
	else if (flag == MIN) {
		while (pp!=nullptr && pp->data>p->data)
		{
			Swap(pp, MIN);
			p = pp;
			pp = GetParent_heap(root, p);
		}
	}
}

BinaryTreeNode<int>* GetParent_heap(BinaryTreeNode<int>* root, BinaryTreeNode<int>* node)
{
	BinaryTreeNode<int> *p = root;
	std::queue<BinaryTreeNode<int> *> q;	//依然是層次遍歷思想
	q.push(p);
	while (!q.empty())
	{
		p = q.front();
		q.pop();
		if (p->left != nullptr) {
			if (p->left == node) {
				return p;
			}
			q.push(p->left);
			if (p->right != nullptr) {
				if (p->right == node) {
					return p;
				}
				q.push(p->right);
			}
		}
	}
	return nullptr;
}

BinaryTreeNode<int>* GetLastParent(BinaryTreeNode<int>* root)
{
	BinaryTreeNode<int> *p = root;
	std::queue<BinaryTreeNode<int> *> q;
	std::stack<BinaryTreeNode<int> *> s;
	BinaryTreeNode<int> *_p = nullptr;
	int count = 0;
	q.push(p);
	while (!q.empty())
	{
		p = q.front();
		q.pop();
		if (p->left != nullptr) {
			q.push(p->left);
			s.push(p);
			if (p->right != nullptr)
				q.push(p->right);
		}
		else if(count==0)
		{
			_p = p;		//記錄第一個葉子節點
			count++;
		}
	}
	BinaryTreeNode<int> *pp = s.top();
	s.pop();
	if (pp->right != nullptr)
		return _p;		//如果最後一個父節點孩子滿了則返回第一個葉子節點
	else
		return pp;
}

測試一下


堆的刪除

同插入一樣,我們將其分為刪除與排序,為了節省排序時間,我們將最後一個節點的值賦給根節點,然後刪除最後一個節點,然後只需對根節點做一次遞迴排序便可,因為這時根節點的左右子樹依然是最大堆。

int DeleteHeap(BinaryTreeNode<int>* root, int flag)
{
	if (root == nullptr) {
		throw std::exception("樹為空");
	}
	BinaryTreeNode<int> *p = root;
	BinaryTreeNode<int> *_p = nullptr;
	std::queue<BinaryTreeNode<int> *> q;
	int ret = root->data;
	q.push(p);
	while (!q.empty())                        //走一趟層次遍歷後指標指向序號最後一個
	{
		p = q.front();
		q.pop();
		if (p->left != nullptr) {
			q.push(p->left);
			if (p->right != nullptr) {
				q.push(p->right);
			}
		}
	}
	_p = GetParent_heap(root, p);            //獲取它的父節點
	if (_p != nullptr) {
		if (_p->left == p) {
			_p->left = nullptr;
		}
		else
		{
			_p->right = nullptr;
		}
		root->data = p->data;            //將其值賦給根節點
		delete p;
	}
	Swap(root, flag);                        //排序
	return ret;
}