1. 程式人生 > >【c++】資料結構———堆

【c++】資料結構———堆

堆是一種特殊的資料結構,它通常是一個可以被看做一棵樹的陣列物件

What?那它到底是一棵樹,還是一個數組呢?答案是陣列。這個陣列以二叉樹的形式來維護。注意:這個二叉樹必須是完全二叉樹

堆結構的二叉樹儲存有兩種:

       最大堆:每個父親結點的值都大於孩子結點。

       最小堆:每個父親結點的值都小於孩子結點。

顧名思義,這種結構就是可以根節點為最大的/最小的結點。不過有一點要注意,堆內的元素並不一定是按陣列下標來排序的,很多

初學者會錯誤的認為最大/最小堆中下標為1的就是第一大/小,下標為2的就是第二大/小。。。。。。

你會問,堆既然是一個數組,那麼它如何以二叉樹的形式來儲存呢?——————答案:下標。

我們先來看一棵二叉樹的模型:(這是一棵亂序的完全二叉樹)紅色數字代表結點的值,黑色數字代表結點的下標。


以上,我們可以看出,每個根結點的下標  =  ((左/右)孩子結點下標-1)/2,左孩子結點下標 = (根結點*2)+1,右孩子結點下

標 = (根結點*2)+ 2。這樣我們用陣列以下標

方式就可以很好的儲存一顆樹了(前提是這棵樹必須是完全二叉樹),我們就會得到一個數組 int a [] = {10,16, 18, 12, 11, 13, 15, 17, 14, 19};

下面我開始講如何建立並維護一個堆,以最大堆為例。

假設,我們現在已經有了一個亂序的堆(例上圖),如何讓它變成最大堆/最小堆呢?————堆的向下調整,向上調整。

向下調整:(調整根結點parent)

我們先要找到陣列中存放的最後一個非葉子結點parent(上圖中的下標為4值為11的結點)。找出它的左孩子leftchild和右孩rightchild

的最大值MaxChild,與父結點parent進行比較,若MaxChild>parent,則將它們倆的值進行交換。然後令parent的下標 = MaxChild下

標。再重複之前的操作。直到leftChild >= 陣列的size。

到這裡,我們才完成了一次向下調整,並不足以構成一個最大堆,在向下調整函式外面我們需要加上一個迴圈,每次向向下調整函式

傳入想調整的根結點。(最後一個非葉子結點只是一個迴圈的‘起點’)

下面看程式碼:

template<typename T>
class Heap
{
public:
	//建堆
	Heap(const T* a, int sz)
	{
		_a.resize(sz);
		for (int i = 0; i < sz; i++)
		{
			_a[i] = a[i];
		}
		for (int i = (_a.size() - 2) / 2; i >= 0; i--)//外層迴圈,每次傳入根結點下標。(_a.size()-2)/2即為最後一個非葉子結點下標。
		{
			AdjustDown(i);
		}
	}
protected:
	//向下調整
	void AdjustDown(int root)
	{
		int parent = root;
		size_t child = parent * 2 + 1;
		while (child < _a.size())//當child>=_a.size()的時候,說明已經越界。
		{
			if (child + 1 < _a.size() && _a[child + 1] > _a[child])
			{
				child++;
			}
			if (_a[child] > _a[parent])
			{
				swap(_a[child], _a[parent]);//交換child與parent
				parent = child;
				child = parent * 2 + 1;
			}
			else
			{
				break;
			}
		}
	}
protected:
	vector<T> _a;//用Vector來代替一個數組
};
向上調整:(調整孩子結點child)

再來看向上調整,就簡單的多了。找到你想調整的結點child,找出它的父親結點parent,比較兩個的大小,若child < parent,不操

作。若child > parent, 則交換兩個的值,然後令child的下標 = parent的下標。重複以上操作,直到child的下標 <= 0(說明到達根結點)。

程式碼:

//向上調整
void AdjustUp(int root)
{
	int child = root;
	int parent = (child - 1) / 2;
	Compare com;
	while (child > 0)
	{
		if (!com(_a[child] ,_a[parent]))
		{
			swap(_a[child], _a[parent]);
			child = parent;
			parent = (child - 1) / 2;
		}
		else
		{
			break;
		}
	}
}

依靠上面兩個演算法,我們就可以建立一個最大堆了,下面我再來講兩個維護堆的演算法。

Push插入

每次都插到陣列最後,然後針對這個結點進行一次向上調整。

//插入
void Push(const T& x)
{
	_a.push_back(x);
	AdjustUp(_a.size() - 1);
}

Pop刪除(刪除根結點,即陣列第一個元素)

有人可能會想,直接讓陣列向前挪動一位就行了。錯!,如果你這麼做,確實達到了刪除的效果,但是你破壞了整個堆的結構。

正確的做法是將根結點(第一個元素)與最後一個結點(最後一個元素)進行交換,刪除最後一個元素,然後再根結點進行一次向下調整。

//刪除
void Pop()
{
	assert(!_a.empty());
	swap(_a[0], _a[_a.size() - 1]);
	_a.pop_back();
	AdjustDown(0);
}


堆有什麼應用?

1、堆排序,時間複雜度為O(N*lgN),最大堆,從小到大排序。最小堆,從大到小排序。

2、優先順序佇列。

3、大資料篩選,一個檔案中包含了1億個隨機數,如何快速找到最大(小)的100W個數字(時間複雜度為O(N*lgk))。

【解析】取前100 萬個整數,構造成了一棵陣列方式儲存的具有小頂堆,然後接著依次取下一個整數,如果它大於最小元素亦即堆頂元素,則將其賦予堆頂元素,然後用Heapify調整整個堆,如此下去,則最後留在堆中的100萬個整數即為所求 100萬個數字。該方法可大大節約記憶體。