資料結構初階--二元樹介紹(基本性質+堆實現順序結構)

2022-11-28 15:00:20

樹的基本概念和結構

樹的相關概念

節點的度:一個節點含有的子樹的個數稱為該節點的度; 如上圖:A的為2
葉節點或終端節點:度為0的節點稱為葉節點; 如上圖:D、F、G、H為葉節點
非終端節點或分支節點:度不為0的節點; 如上圖:A、B…等節點為分支節點
雙親節點或父節點:若一個節點含有子節點,則這個節點稱為其子節點的父節點; 如上圖:A是B的父節點
孩子節點或子節點:一個節點含有的子樹的根節點稱為該節點的子節點; 如上圖:B是A的孩子節點
兄弟節點:具有相同父節點的節點互稱為兄弟節點; 如上圖:B、C是兄弟節點
樹的度:一棵樹中,最大的節點的度稱為樹的度; 如上圖:樹的度為2
節點的層次:從根開始定義起,根為第1層,根的子節點為第2層,以此類推;
樹的高度或深度:樹中節點的最大層次; 如上圖:樹的高度為4(根節點的高度記為1)
堂兄弟節點:雙親在同一層的節點互為堂兄弟;如上圖:H、I互為兄弟節點
節點的祖先:從根到該節點所經分支上的所有節點;如上圖:A是所有節點的祖先
子孫:以某節點為根的子樹中任一節點都稱為該節點的子孫。如上圖:所有節點都是A的子孫
森林:由m(m>0)棵互不相交的樹的集合稱為森林樹的表示——左孩子右兄弟表示法

樹的表示——左孩子右兄弟表示法

樹的表示方法有很多,由於樹不是一種線性的結構,所以表示起來會顯得有些複雜,最常用的就是左孩子右兄弟表示法
左孩子右兄弟表示法是節點中儲存第一個孩子的節點的指標,還有一個指標指向下一個兄弟節點。

template <class DateType>
struct Node
{
	Node* firstChild; // 第一個孩子結點
	Node* pNextBrother; // 指向其下一個兄弟結點
	DataType data; // 結點中的資料域
};

二元樹的概念及性質

二元樹的概念

二元樹是n個有限元素的集合,該集合或者為空、或者由一個稱為根(root)的元素及兩個不相交的、被分別稱為左子樹和右子樹的二元樹組成,是有序樹。當集合為空時,稱該二元樹為空二元樹。在二元樹中,一個元素也稱作一個結點。

注意

  1. 二元樹的度不超過2
  2. 二元樹的子樹有左右之分,次序不能顛倒,因此二元樹是有序樹

特殊的二元樹

滿二元樹:一個二元樹,如果每一個層的結點數都達到最大值,則這個二元樹就是滿二元樹。也就是說,如果一個二元樹的層數為K,且結點總數是(2^k) -1 ,則它就是滿二元樹。

完全二元樹:一棵深度為k的有n個結點的二元樹,對樹中的結點按從上至下、從左到右的順序進行編號,如果編號為i(1≤i≤n)的結點與滿二元樹中編號為i的結點在二元樹中的位置相同,則這棵二元樹稱為完全二元樹。

二元樹的性質

  • 若規定根節點的層數為1,則一棵非空二元樹的第n層上最多有2^(n-1)個結點
  • 若規定根節點的層數為1,則深度為n的二元樹的最大結點數是2^n-1
  • 對任何一棵二元樹, 如果度為0其葉結點個數為 , 度為2的分支結點個數為 ,則有n0 = n2+1
  • 樹中父節點與子節點的關係
    • leftChild = parent*2+1
    • rightChild = parent*2+1
    • parent = (child-1)/2

二元樹的順序結構及實現

二元樹的順序結構

普通的二元樹是不適合用陣列來儲存的,因為可能會存在大量的空間浪費。而完全二元樹更適合使用順序結構儲存。

可以看出,只有完全二元樹可以很充分地利用空間,普通二元樹會浪費很大的空間。

堆的概念以及結構

堆的性質:

  • 堆中某個結點的值總是不大於或不小於其父結點的值;
  • 堆總是一棵完全二元樹。

堆的實現(小堆為例)

堆的框架

由於堆是用陣列來進行儲存的,所以這裡的結構和順序表有些類似,邏輯上是堆,物理上是一種陣列的形式。

template <class DateType>
//小堆的實現
class MinHeap
{
public:
private:
	int size;
	int capacity;
	DateType* data;
};

堆的初始化

堆的初始化和順序表的很相似基本上什麼都不用做,只要指標置空,大小和容量置0即可。

//初始化堆,指標置空,大小和容量置0即可
	MinHeap()
	{
		this->size = this->capacity = 0;
		this->data = NULL;
	}
	//初始化堆,大小為n
	MinHeap(int n)
	{
		this->data = new DateType[n];
		this->capacity = n;
		this->size = 0;
	}

交換函數

//交換函數
	void Swap(DateType* x, DateType* y)
	{
		DateType tmp = *x;
		*x = *y;
		*y = tmp;
	}

向下調整演演算法

演演算法作用:將一個根節點的左右孩子均為大堆(小堆)的完全二元樹(非堆)堆調整成大堆(小堆)。

演演算法思路:以調整小堆為例,從根結點處開始,選出左右孩子中值較小的孩子。讓小的孩子與其父親進行比較。若小的孩子比父親還小,則該孩子與其父親的位置進行交換。並將原來小的孩子的位置當成父親繼續向下進行調整,直到調整到葉子結點為止。若小的孩子比父親大,則不需處理了,調整完成,整個樹已經是小堆了。

前提條件:對於大堆,根節點的左右孩子都必須是一個大堆;對於小堆,根節點的左右孩子都必須是小堆。

具體例子:

//向下調整演演算法(小堆)
	void AdjustDown(int n, int parent)
	{
		//child記錄左右函數中值較小的孩子的下標
		int child = 2 * parent + 1;//先預設其左孩子的值比較小
		while (child < n)
		{
			//右孩子存在並且比左孩子還小
			if (child + 1 < n && data[child + 1] < data[child])
			{
				child++;//較小的孩子改為右孩子
			}
			//左右孩子中較小孩子的值比父結點還小
			if (data[child] < data[parent])
			{
				//將父結點和較小的子結點交換
				Swap(&data[child], &data[parent]);
				//繼續向下進行調整
				parent = child;
				child = 2 * parent + 1;
			}
			else
			{
				//已成堆
				break;
			}
		}
	}

向上調整演演算法

演演算法作用:向上調整演演算法就是在插入一個節點後為了使堆依舊保持原來的大堆或者小堆的一個調整演演算法。先將該節點與父親節點比較,如果比父親節點大就交換(原本是大堆)否則就不交換,直到交換到根節點為止。

當我們在一個堆的末尾插入一個資料後,需要對堆進行調整,使其仍然是一個堆,這時需要用到堆的向上調整演演算法。

演演算法思路:

將目標結點與其父結點比較。若目標結點的值比其父結點的值小,則交換目標結點與其父結點的位置,並將原目標結點的父結點當作新的目標結點繼續進行向上調整。若目標結點的值比其父結點的值大,則停止向上調整,此時該樹已經是小堆了。

具體例子:

//向上調整演演算法(小堆)
	void AdjustUp(int child)
	{
		int parent = (child - 1) / 2;
		//調整到根結點的位置截止
		while (child > 0)
		{
			//孩子結點的值小於父結點的值
			if (data[child] < data[parent])
			{
				//將父結點與孩子結點交換
				Swap(&data[child], &data[parent]);
				//繼續向上進行調整
				child = parent;
				parent = (child - 1) / 2;
			}
			else//已成堆
			{
				break;
			}
		}
	}

堆的建立

對於給定的一個陣列,我們如何把他構建成大堆或者小堆呢?

堆的構建有兩種方法:
第一種:從最後一個非葉子節點開始向下調整(從後往前遍歷,向下調整)

第二種:從第二個節點往後開始向上調整(從前往後遍歷,向上調整)

為什麼呢?答案很簡單,因為堆的向下和向上調整要求左右子樹必須都是堆,只有這樣才能保證一個無序的樹,按照這種方式遍歷,它的左右子樹是一個堆。

上面說到,使用堆的向下調整演演算法需要滿足其根結點的左右子樹均為大堆或是小堆才行,那麼如何才能將一個任意樹調整為堆呢?
方法一,我們只需要從倒數第一個非葉子結點開始,從後往前,按下標,依次作為根去向下調整即可。

注意:最後一個非葉子節點的計算:假設一共有n個節點,最後一個節點的小標為n-1,最後一個非葉子節點就是最後一個節點的父節點,因此,最後一個非葉子節點的下標為:(n-2)/2

方法二:我們只需要從正數第二個結點開始,從前向後,按下標,依次向上調整即可

第一種程式碼實現

//建立堆
	//利用向下調整演演算法建立堆(小堆)
	void HeapCreate_down(int n)
	{
		//利用向下調整演演算法,從第一個非葉子結點開始調整。
		int i = (n - 2) / 2;
		//建小堆 排降序  建大堆 排升序
		for (; i >= 0; i--)
		{
			AdjustDown(i, n);
		}
	}

第二種程式碼實現

//利用向上調整演演算法建立堆(小堆)
	void HeapCreate_up(int n)
	{
		int i = 0;
		//建小堆 排降序  建大堆 排升序
		for (i = 1; i < n; i++)
		{
			//建大堆 向下調整
			AdjustUp(i);
		}
	}

堆的插入

堆的插入和順序表的尾插有些相似,要考慮擴容的問題,有一點不同的是堆在插入後要進行向上調整,也就是向上調整演演算法,保持原來的堆的性狀。

void HeapPush(DateType val)
	{
		if (this->capacity == this->size)
		{
			int newCapacity = this->capacity == 0 ? 4 : 2 * this->capacity;
			DateType* tmp = (DateType*)realloc(this->data, newCapacity * sizeof(DateType));
			if (tmp == NULL)
			{
				cout << "realloc分配失敗" << endl;
				exit(-1);
			}
			this->data = tmp;
			this->capacity = newCapacity;
		}
		this->size++;
		this->data[this->size - 1] = val;
		//向上調整
		AdjustUp(this->size - 1);
	}

堆的刪除

我們規定,堆的刪除在頭部進行,所以堆的刪除也和順序表的頭刪有些相似,要對大小進行斷言,確保堆的大小不為0。但是堆不能直接在頭部進行刪除,這樣會破壞堆的結構,又要重新建堆的時間複雜度是O(n)(後面會證明),這樣就顯得很麻煩。
於是就有新的一種方法,把堆頂的資料和堆尾的資料先進行交換,然後再把堆尾的資料刪除,這樣堆的結構就沒有完全破壞,因為堆頂的左子樹和右子樹都是大堆,我們可以進行向下調整就可以恢復堆的形狀了,向下調整演演算法的時間複雜度是堆的高度次,即O(log(h+1))。顯然,下面這種演演算法更優。

程式碼實現如下:

//堆的刪除
	void HeapPop()
	{
		if (HeapEmpty())
		{
			cout << "空堆,無法刪除" << endl;
		}
		//把最後一個數替換堆頂的數,然後再進行向下調整
		Swap(&data[0], &data[this->size - 1]);
		this->size--;
		//向下調整
		AdjustDown(this->size, 0);
	}

堆的元素個數

//堆的元素個數
	int HeapSize()
	{
		return this->size;
	}

堆的銷燬

堆的銷燬就是對動態申請的空間進行釋放,防止記憶體漏失,其實和順序表的銷燬很相似。

//堆的銷燬
	void HeapDestroy()
	{
		delete[] this->data;
		size = capacity = 0;
	}

列印堆的資料

void PrintHeap()
	{
		for (int i = 0; i < this->size; i++)
		{
			cout << data[i] << " ";
		}
		cout << endl;
	}

完整程式碼以及測試

#define _CRT_SECURE_NO_WARNINGS
#include<iostream> //引入標頭檔案
#include<string>//C++中的字串
using namespace std; //標準名稱空間
template <class DateType>
//小堆的實現
class MinHeap
{
public:
	//初始化堆,指標置空,大小和容量置0即可
	MinHeap()
	{
		this->size = this->capacity = 0;
		this->data = NULL;
	}
	//初始化堆,大小為n
	MinHeap(int n)
	{
		this->data = new DateType[n];
		this->capacity = n;
		this->size = 0;
	}
	//交換函數
	void Swap(DateType* x, DateType* y)
	{
		DateType tmp = *x;
		*x = *y;
		*y = tmp;
	}
	//向下調整演演算法(小堆)
	void AdjustDown(int n, int parent)
	{
		//child記錄左右函數中值較小的孩子的下標
		int child = 2 * parent + 1;//先預設其左孩子的值比較小
		while (child < n)
		{
			//右孩子存在並且比左孩子還小
			if (child + 1 < n && data[child + 1] < data[child])
			{
				child++;//較小的孩子改為右孩子
			}
			//左右孩子中較小孩子的值比父結點還小
			if (data[child] < data[parent])
			{
				//將父結點和較小的子結點交換
				Swap(&data[child], &data[parent]);
				//繼續向下進行調整
				parent = child;
				child = 2 * parent + 1;
			}
			else
			{
				//已成堆
				break;
			}
		}
	}
	//向上調整演演算法(小堆)
	void AdjustUp(int child)
	{
		int parent = (child - 1) / 2;
		//調整到根結點的位置截止
		while (child > 0)
		{
			//孩子結點的值小於父結點的值
			if (data[child] < data[parent])
			{
				//將父結點與孩子結點交換
				Swap(&data[child], &data[parent]);
				//繼續向上進行調整
				child = parent;
				parent = (child - 1) / 2;
			}
			else//已成堆
			{
				break;
			}
		}
	}
	//建立堆
	//利用向下調整演演算法建立堆(小堆)
	void HeapCreate_down(int n)
	{
		//利用向下調整演演算法,從第一個非葉子結點開始調整。
		int i = (n - 2) / 2;
		//建小堆 排降序  建大堆 排升序
		for (; i >= 0; i--)
		{
			AdjustDown(i, n);
		}
	}
	//利用向上調整演演算法建立堆(小堆)
	void HeapCreate_up(int n)
	{
		int i = 0;
		//建小堆 排降序  建大堆 排升序
		for (i = 1; i < n; i++)
		{
			//建大堆 向下調整
			AdjustUp(i);
		}
	}
	//堆的插入
	void HeapPush(DateType val)
	{
		if (this->capacity == this->size)
		{
			int newCapacity = this->capacity == 0 ? 4 : 2 * this->capacity;
			DateType* tmp = (DateType*)realloc(this->data, newCapacity * sizeof(DateType));
			if (tmp == NULL)
			{
				cout << "realloc分配失敗" << endl;
				exit(-1);
			}
			this->data = tmp;
			this->capacity = newCapacity;
		}
		this->size++;
		this->data[this->size - 1] = val;
		//向上調整
		AdjustUp(this->size - 1);
	}
	//堆的刪除
	void HeapPop()
	{
		if (HeapEmpty())
		{
			cout << "空堆,無法刪除" << endl;
		}
		//把最後一個數替換堆頂的數,然後再進行向下調整
		Swap(&data[0], &data[this->size - 1]);
		this->size--;
		//向下調整
		AdjustDown(this->size, 0);
	}
	//判斷堆是否為空
	int HeapEmpty()
	{
		return this->size == 0;
	}
	//堆的元素個數
	int HeapSize()
	{
		return this->size;
	}
	//堆的銷燬
	void HeapDestroy()
	{
		delete[] this->data;
		size = capacity = 0;
	}
	//列印堆的資料
	void PrintHeap()
	{
		for (int i = 0; i < this->size; i++)
		{
			cout << data[i] << " ";
		}
		cout << endl;
	}
private:
	int size;
	int capacity;
	DateType* data;
};
int main()
{
	MinHeap<int> minheap;
	minheap.HeapPush(12);
	minheap.HeapPush(43);
	minheap.HeapPush(56);
	minheap.HeapPush(11);
	minheap.HeapPush(2);
	minheap.HeapPush(35);
	cout << minheap.HeapSize() << endl;
	minheap.PrintHeap();
	cout << "------------------" << endl;
	minheap.HeapPop();
	minheap.HeapPop();
	cout << minheap.HeapSize() << endl;
	minheap.PrintHeap();
	cout << "------------------" << endl;
	MinHeap<int> minheap2(3);
	minheap2.HeapPush(24);
	minheap2.HeapPush(56);
	minheap2.HeapPush(11);
	minheap2.HeapPush(29);
	minheap2.HeapPush(1);
	minheap2.HeapPush(38);
	minheap2.HeapPush(22);
	cout << minheap2.HeapSize() << endl;
	minheap2.PrintHeap();
	system("pause");
	return EXIT_SUCCESS;
}