二叉樹由淺至深(上)

1.數概念及結構

1.1樹的概念

  • 樹是一種非線性的數據結構,它是由n(n>=0)個有限結點組成一個具有層次關係的集合。
  • 樹具有以下的特點:

每個結點有零個或多個子結點;
沒有父結點的結點稱爲根結點
每一個非根結點有且只有一個父結點;
除了根結點外,每個子結點可以分爲多個不相交的子樹

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

1.2樹的表示

樹有很多種表示方式,如:雙親表示法,孩子表示法、孩子兄弟表示法等等。這裏簡單瞭解其中最常用的孩子兄弟表示法。

typedef int DataType;
struct Node
{
    struct Node* _firstChild1;    // 第一個孩子結點
    struct Node* _pNextBrother;   // 指向其下一個兄弟結點
    DataType _data;               // 結點中的數據域
};

在這裏插入圖片描述

1.3樹在實際中的運用

在這裏插入圖片描述

2.二叉樹概念及結構

2.1概念

  • 一棵二叉樹是結點的一個有限集合,該集合或者爲空,或者是由一個根節點加上兩棵別稱爲左子樹和右子樹的二叉樹組成。
  • 二叉樹的特點
  1. 每個結點最多有兩棵子樹,即二叉樹不存在度大於2的結點
  2. 二叉樹的子樹有左右之分,其子樹的次序不能顛倒

2.2特殊的二叉樹:

  1. 滿二叉樹:一個二叉樹,如果每一個層的結點數都達到最大值,則這個二叉樹就是滿二叉樹。也就是說,如果一個二叉樹的層數爲K,且結點總數是(2^k) -1 ,則它就是滿二叉樹。
  2. 完全二叉樹:對於深度爲K的,有n個結點的二叉樹,當且僅當其每一個結點都與深度爲K的滿二叉樹中編號從1至n的結點一一對應時稱之爲完全二叉樹。 要注意的是滿二叉樹是一種特殊的完全二叉樹在這裏插入圖片描述

2.3 二叉樹的存儲結構

二叉樹一般可以使用兩種結構存儲,一種順序結構,一種鏈式結構

2.3.1 順序存儲:

順序結構存儲就是使用數組來存儲,一般使用數組只適合表示完全二叉樹,因爲不是完全二叉樹會有空間的浪費。二叉樹順序存儲在物理上是一個數組,在邏輯上是一顆二叉樹。
在這裏插入圖片描述

2.5.2 鏈式存儲:

二叉樹的鏈式存儲結構是指,用鏈表來表示一棵二叉樹,即用鏈來指示元素的邏輯關係。 通常的方法是鏈表中每個結點由三個域組成,數據域和左右指針域,左右指針分別用來給出該結點左孩子和右孩子所在的鏈結點的存儲地址 。鏈式結構又分爲二叉鏈和三叉鏈
在這裏插入圖片描述
在這裏插入圖片描述

// 二叉鏈
struct BinaryTreeNode
{
    struct BinTreeNode* _pLeft;   // 指向當前節點左孩子
    struct BinTreeNode* _pRight; // 指向當前節點右孩子
    BTDataType _data; // 當前節點值域
}
 
// 三叉鏈
struct BinaryTreeNode
{
    struct BinTreeNode* _pParent; // 指向當前節點的雙親
    struct BinTreeNode* _pLeft;   // 指向當前節點左孩子
    struct BinTreeNode* _pRight; // 指向當前節點右孩子
    BTDataType _data; // 當前節點值域
}

3.二叉樹的順序結構及實現

3.1 二叉樹的順序結構

普通的二叉樹是不適合用數組來存儲的,因爲可能會存在大量的空間浪費。而完全二叉樹更適合使用順序結構存儲。現實中我們通常把堆使用順序結構的數組來存儲。
在這裏插入圖片描述

3.2 堆的概念及結構

如果有一個關鍵碼的集合K = {k0,k1, k2,…,kn-1},把它的所有元素按完全二叉樹的順序存儲方式存儲在一個一維數組中,並滿足:Ki <= K2i+1 且 Ki<= K2i+2 (Ki >= K2i+1 且 Ki >= K2i+2) i = 0,1,2…,則稱爲小堆(或大堆)。將根節點最大的堆叫做最大堆或大根堆,根節點最小的堆叫做最小堆或小根堆

  • 堆的性質:
    ①堆中某個節點的值總是不大於或不小於其父節點的值;
    ②堆總是一棵完全二叉樹。
    在這裏插入圖片描述

3.2 堆的實現

3.2.1 堆向下調整算法

現在我們給出一個數組,邏輯上看做一顆完全二叉樹。我們通過從根節點開始的向下調整算法可以把它調整成一個小堆。向下調整算法有一個前提:左右子樹必須是一個堆,才能調整。

int a[] = {5,163,2017,4};

在這裏插入圖片描述

3.2.2堆的創建

下面我們給出一個數組,這個數組邏輯上可以看做一顆完全二叉樹,但是還不是一個堆,現在我們通過算法,把它構建成一個堆。根節點左右子樹不是堆,需要從倒數的第一個非葉子節點的子樹開始調整,一直調整到根節點的樹,就可以調整成堆。

int a[] = {1,5,3,8,7,6};

在這裏插入圖片描述

3.2.3 堆的插入

先插入一個80到數組的尾上,再進行向上調整算法,直到滿足堆。
在這裏插入圖片描述

3.2.4 堆的刪除

刪除堆是刪除堆頂的數據,將堆頂的數據根最後一個數據一換,然後刪除數組最後一個數據,再進行向下調整算法。
在這裏插入圖片描述

3.2.5 堆排序

在這裏插入圖片描述

3.2.6 堆的代碼實現

#include<stdio.h>
#include<assert.h>
#include<malloc.h>
#include<time.h>
#include<memory>
typedef int HPDataType;
typedef struct Heap
{
	HPDataType* _array;
	int _size;
	int _capacity;
}Heap;

void Swap(HPDataType* a, HPDataType* b)
{
	assert(a);
	assert(b);
	HPDataType temp = *a;
	*a = *b;
	*b = temp;
}

void AdjustDown(HPDataType* a, int n, int root)//向下調整
{
	assert(a);
	int parent = root;
	int child = 2 * parent + 1;//左孩子

	while (child < n)//只要孩子不是葉子結點,就繼續調整
	{
		if (child + 1 < n && a[child + 1] < a[child])//保證a[child]裏面是最小的孩子
		{
			++child;
		}

		if (a[child] < a[parent])
		{
			Swap(&a[child], &a[parent]);//保證a[parent]比兩個孩子都小

			//重新更新parent、child,即繼續判斷被交換後的結點
			parent = child;
			child = 2 * parent + 1;
		}
		else//當前結點<最小孩子
		{
			break;
		}
	}
}

void AdjustUp(HPDataType* a, int n, int child)//向上調整
{
	assert(a);
	int parent = (child - 1) / 2;

	while (child > 0)//只要這個結點不到根結點,就一直向上調整
	{
		if (a[child] < a[parent])
		{
			Swap(&a[child], &a[parent]);

			//更新被調整的結點
			child = parent;
			parent = (child - 1) / 2;
		}
		else
		{
			break;
		}
	}
}

void HeapInit(Heap* hp, HPDataType* a, int n)//創建堆(用數組初始化數組,並創建堆)
{
	assert(hp && a);
	//先將數組內容拷貝到堆裏的數組
	hp->_array = (HPDataType*)malloc(n*sizeof(HPDataType));
	hp->_size = hp->_capacity = n;
	memcpy(hp->_array, a, n*sizeof(HPDataType));

	//建堆(我們以小堆爲例)
	//從最後一個節點的父結點開始向下調整,因爲葉子結點不用調整,而最後一個
	//非葉子結點就是最後一個節點的父結點。這樣從下->上調整,保證每個結點都是小堆。
	//向下調整的思想是:若當前節點不滿足小堆性質,即當前結點>它的左右孩子最小的
	//那個,就將該結點與那個孩子交換,然後判斷被交換後的結點...直到葉子結點
	int end = hp->_size - 1;//最後一個元素下標
	for (int i = (end - 1) / 2; i >= 0; i--)
	{
		//最後一個節點的父結點開始到根結點,逐個做向下調整
		AdjustDown(hp->_array, hp->_size, i);
	}
}
//注意如果建大堆的話,只需要將向下調整函數AdjustDown()中
//稍作修改即可,找最大孩子,當父結點小於最大孩子,將其交換...

void HeapDeStory(Heap* hp)//銷燬
{
	assert(hp);
	free(hp->_array);
	hp->_array = NULL;
	hp->_size = hp->_capacity = 0;
}

void HeapPrint(Heap* hp)//打印堆
{
	assert(hp);
	for (int i = 0; i < hp->_size; i++)
	{
		printf("%d ", hp->_array[i]);
	}
	printf("\n");
}


//插入一個數據後,堆結構不改變(原來是大堆,刪除完堆頂元素後,仍然是大堆)
void HeapPush(Heap* hp, HPDataType x)
{
	assert(hp);
	//堆插入一個元素,物理上實際是數組的尾插。要想保證堆結構不變,
	//就需要從該元素開始向上調整,直到它到根結點。
	//向上調整的思想是:只要這個結點<它的父結點,將二者交換,
	//然後再判斷交換後的節點...
	if (hp->_size == hp->_capacity)//需要增容
	{
		hp->_capacity *= 2;
		HPDataType* tmp = (HPDataType*)realloc(hp->_array, hp->_capacity * sizeof(HPDataType));
		if (tmp)
		{
			hp->_array = tmp;
		}
		hp->_array[hp->_size] = x;
		hp->_size++;
	}
	//將這個元素向上調整(注意最後一個參數傳的是下標)
	AdjustUp(hp->_array, hp->_size, hp->_size - 1);
}
void HeapPop(Heap* hp)//刪除堆頂後,堆結構不改變
{
	assert(hp);
	//將堆頂元素與最後一個元素交換,將元素數目減1(刪除的是堆頂元素)
	//然後對堆頂做一次向下調整
	Swap(&hp->_array[0], &hp->_array[hp->_size - 1]);
	hp->_size--;

	AdjustDown(hp->_array, hp->_size, 0);
}
HPDataType HeapTop(Heap* hp)//返回堆頂元素
{
	assert(hp);
	return hp->_array[0];
}

int HeapSize(Heap* hp)//返回堆中元素個數
{
	assert(hp);
	return hp->_size;
}
int HeapEmpty(Heap* hp)//判空(空返回0)
{
	assert(hp);
	return hp->_size == 0 ? 0 : 1;
}

void HeapSort(int* a, int n)//堆排序(升序)
{
	assert(a);
	//如果是升序,則建大堆。建好大堆後,將首尾元素交換(最大的交換到最後),
	//再將首元素位置做一次向下調整...(不過需要注意的是,每將最大的一個值交換
	//到最後時,下一次的向下調整數據長度減1,即排好的數據就不用管了)

	//建大堆
	int end = n - 1;
	for (int i = (end - 1) / 2; i >= 0; i--)
	{
		AdjustDown(a, n, i);
	}

	while (end > 0)//最後一個元素下標不到第一個元素,就一直交換,調整...
	{
		Swap(&a[0], &a[end]);//交換首尾元素
		AdjustDown(a, end, 0);//首元素向下調整
		end--;
	}
}

void TopK()
{
	//假設再N個數中選取出最大的前K個數.
	//選取N個數的前K個數建一個小堆,然後用N中剩下的數據與堆頂元素相比,
	//如果大於堆頂元素,先將堆頂元素pop掉(pop()函數仍然保證堆結構),
	//然後將該元素入堆(push()函數仍然保證堆性質)

	const int N = 10000;
	const int K = 100;
	int* a = (int*)malloc(N * sizeof(int));

	srand((unsigned int)time(NULL));
	for (int i = 0; i < N; i++)
	{
		a[i] = rand() % 100000;//將數組元素初始化爲0-10萬之間的隨機值
	}

	//爲了好測試,我們賦10個大於100000的值
	for (int j = 5; j < 15; j++)
	{
		a[j] = 100000 + j;
	}

	//用前K個數字建小堆
	Heap h;
	HeapInit(&h, a, K);
	for (int i = K; i < N; ++i)//將剩下的元素依次與堆頂元素比較,大的入堆
	{
		if (a[i] > HeapTop(&h))
		{
			HeapPop(&h);
			HeapPush(&h, a[i]);
		}
	}

	//最後堆中的K個元素,就是最大的前K個數據
	HeapPrint(&h);
}

void Test()
{
	Heap h;
	int a[] = { 2, 7, 8, 0, 0, 7, 5, 3, 7, 9 };
	int sz = sizeof(a) / sizeof(a[0]);
	HeapInit(&h, a, sz);
	HeapPrint(&h);

	//HeapPush(&h, 0);
	//HeapPrint(&h);

	//HeapPop(&h);
	//HeapPrint(&h);
	HeapSort(a, sz);
	for (int i = 0; i < sz; i++)
	{
		printf("%d ", a[i]);
	}
	printf("\n");

	TopK();
}

4.二叉樹鏈式結構的實現

4.1二叉樹鏈式結構的遍歷

所謂遍歷是指沿着某條搜索路線,依次對樹中每個結點均做一次且僅做一次訪問。訪問結點所做的操作依賴於具體的應用問題。 遍歷是二叉樹上最重要的運算之一,是二叉樹上進行其它運算之基礎。
在這裏插入圖片描述
前序/中序/後序的遞歸結構遍歷:是根據訪問結點操作發生位置命名

  1. NLR:前序遍歷(亦稱先序遍歷)——訪問根結點的操作發生在遍歷其左右子樹之前。
    實現:
	/////遞歸實現
    vector<int> preorderTraversal(TreeNode* root) {
        vector<int> res;
        ProTraversal(root,res);
        return res;
    }
    void ProTraversal(TreeNode* root,vector<int>& ans)
    	{
         if(root==NULL) return ;
       	 ans.emplace_back(root->val);
         ProTraversal(root->left,ans);
         ProTraversal(root->right,ans);
        }
   }
   
   //////迭代實現
   vector<int> preorderTraversal(TreeNode* root) {
        stack<TreeNode*> _stk;
        vector<int> ans;
        TreeNode* temp=root;
        while(temp!=NULL||!_stk.empty()){
            while(temp!=NULL){
                ans.push_back(temp->val);
                _stk.push(temp);
                temp=temp->left;
            }
            temp=_stk.top();
            _stk.pop();
            temp=temp->right;
        }
        return ans;
    }
  1. LNR:中序遍歷——訪問根結點的操作發生在遍歷其左右子樹之中(間)。
	///////遞歸實現
    void InorderTraversal(TreeNode* root,vector<int>& ans){
        if(root==NULL)
            return;
        InorderTraversal(root->left,ans);
        ans.emplace_back(root->val);
        InorderTraversal(root->right,ans);
    }
    vector<int> inorderTraversal(TreeNode* root) {
        vector<int> ans;
        InorderTraversal(root,ans);
        return ans;
    }

	/////迭代實現
    vector<int> inorderTraversal(TreeNode* root) {
        vector<int> res;
        stack<TreeNode*> s;
        while(root || s.size() > 0){
            while(root){
                s.push(root);
                root = root->left;
            }
            root = s.top(); 
            s.pop();
            res.push_back(root->val);
            root = root->right;
        }
        return res;
    }
  1. LRN:後序遍歷——訪問根結點的操作發生在遍歷其左右子樹之後。
	///////迭代實現
vector<int> postorderTraversal(TreeNode* root) {
        vector<int> ans;
        stack<TreeNode*> stk;
        TreeNode* cur = root;
        TreeNode* rightchild = NULL;
        while(cur || !stk.empty()){
            while(cur != NULL){
                stk.push(cur);
                cur = cur -> left;
            }
            cur = stk.top();
            if(!cur -> right|| rightchild == cur -> right){
                ans.push_back(cur -> val);
                stk.pop();
                rightchild = cur;
                cur = NULL;
            }
            else{
                rightchild = NULL;
                cur = cur -> right;
            }
        }
        return ans;
    }
    
///////遞歸實現
    vector<int> postorderTraversal(TreeNode* root,vector<int> ans) {
        if(!root)   return ans;
        postorderTraversal(root -> left);
        postorderTraversal(root -> right);
        ans.push_back(root -> val);
        return ans;
    }
  1. 層序遍歷:除了先序遍歷、中序遍歷、後序遍歷外,還可以對二叉樹進行層序遍歷。設二叉樹的根節點所在層數爲1,層序遍歷就是從所在二叉樹的根節點出發,首先訪問第一層的樹根節點,然後從左到右訪問第2層上的節點,接着是第三層的節點,以此類推,自上而下,自左至右逐層訪問樹的結點的過程就是層序遍歷。
////////遞歸實現:
vector<vector<int>> res;   
    vector<vector<int>> levelOrder(TreeNode* root) 
    {
        addVector(root,0);      //調用遞歸函數
        return res;
    }
  
    void addVector(TreeNode* root,int level)
    {
        if(root == NULL)    return;
        if(res.size()==level)       res.resize(level+1);    //level表示層數,也對應二維數組的第一層索引,
        res[level].push_back(root->val);
        addVector(root->left,level+1);
        addVector(root->right,level+1);
    }

////////迭代實現
vector<vector<int>> levelOrder(TreeNode* root) {
        vector<vector<int>> res;
        
        if (root == NULL)   return {};  
        queue<TreeNode*> q;     
        q.push(root);  

        while (!q.empty()) 
        {  
            vector<int>level;                //存放每一層的元素值
            int count=q.size();             //隊列大小表示當前層數的元素個數
            while(count--)                  //count--逐個對該層元素進行處理
            {
            TreeNode *t=q.front();             q.pop();     
            level.push_back(t->val);
            if(t->left)     q.push(t->left);
            if(t->right)    q.push(t->right);
            }

            res.push_back(level);           //將當層元素的vector加入res中
        }
        return res;
        }

由於被訪問的結點必是某子樹的根,所以N(Node)、L(Left subtree)和R(Right subtree)又可解釋爲根、根的左子樹和根的右子樹。NLR、LNR和LRN分別又稱爲先根遍歷、中根遍歷和後根遍歷。
在這裏插入圖片描述
在這裏插入圖片描述

4.3 二叉樹的基本補充實現

#include<stdio.h>
#include<assert.h>
#include<stack>
#include<vector>
#include<iostream>
using namespace std;

typedef char DataType;
typedef struct TreeNode
{
	DataType elem;
	struct TreeNode* rchild;
	struct TreeNode* lchild;
}TreeNode;

void InitTree(TreeNode** root)//二叉樹的初始化
{
	assert(root);
	if (*root == NULL)
		return;
	*root = NULL;
	return;
}

//二叉樹前序遍歷遞歸版本
void PreOrder(TreeNode* root)
{
	if (root == NULL)
		return;
	printf("%c ", root->elem);
	PreOrder(root->lchild);
	PreOrder(root->rchild);
	return;
}
//二叉樹前序遍歷非遞歸版本
vector<int> preorderTraversal(TreeNode* root) {
	stack<TreeNode*> _stk;
	vector<int> ans;
	TreeNode* temp = root;
	while (temp != NULL || !_stk.empty()){
		while (temp != NULL){
			ans.push_back(temp->elem);
			_stk.push(temp);
			temp = temp->lchild;
		}
		temp = _stk.top();
		_stk.pop();
		temp = temp->rchild;
	}
	return ans;
}

////中序遍歷遞歸版本
void InOrder(TreeNode* root)
{
	if (root == NULL)
		return;
	InOrder(root->lchild);
	printf("%c ", root->elem);
	InOrder(root->rchild);
	return;
}
/////中序遍歷非遞歸實現
vector<int> inorderTraversal(TreeNode* root) {
	vector<int> res;
	stack<TreeNode*> s;
	while (root || s.size() > 0){
		while (root){
			s.push(root);
			root = root->lchild;
		}
		root = s.top();
		s.pop();
		res.push_back(root->elem);
		root = root->rchild;
	}
	return res;
}

////後序遍歷遞歸版本
void PostOrder(TreeNode* root)
{
	if (root == NULL)
		return;
	PostOrder(root->lchild);
	PostOrder(root->rchild);
	printf("%c ", root->elem);
	return;
}
////後序遍歷非遞歸實現
vector<int> postorderTraversal(TreeNode* root) {
	vector<int> ans;
	stack<TreeNode*> stk;
	TreeNode* cur = root;
	TreeNode* rightchild = NULL;
	while (cur || !stk.empty()){
		while (cur != NULL){
			stk.push(cur);
			cur = cur->lchild;
		}
		cur = stk.top();
		if (!cur->rchild || rightchild == cur->rchild){
			ans.push_back(cur->elem);
			stk.pop();
			rightchild = cur;
			cur = NULL;
		}
		else{
			rightchild = NULL;
			cur = cur->rchild;
		}
	}
	return ans;
}

////二叉樹的創建
TreeNode* _CreateTree(DataType array[], size_t size, DataType null_node, size_t* index)
{
	assert(index);
	if (*index >= size)
		return NULL;
	if (array[*index] == null_node)
	{
		++*index;
		return NULL;
	}
	TreeNode* new_node = _CreateTree(array[(*index)++]);
	new_node->lchild = _CreateTree(array, size, null_node, index);
	new_node->rchild = _CreateTree(array, size, null_node, index);
	return new_node;
}
TreeNode* CreateTree(DataType array[], size_t size, DataType null_node)
{
	assert(array);
	size_t index = 0;
	return _CreateTree(array, size, null_node, &index);
}


///二叉樹的拷貝
TreeNode* TreeClone(TreeNode* root)
{
	if (root == NULL)
		return NULL;
	TreeNode* new_node = _CreateTree(root->elem);
	new_node->lchild = TreeClone(root->lchild);
	new_node->rchild = TreeClone(root->rchild);
	return new_node;
}

/////二叉樹的銷燬
void DestroyNode(TreeNode* node)
{
	free(node);
	return;
}
void TreeDestroy(TreeNode** root)
{
	assert(root);
	if (*root == NULL)
		return;
	TreeDestroy(&((*root)->lchild));
	TreeDestroy(&((*root)->rchild));
	DestroyNode(*root);
	*root = NULL;
	return;
}

///////求二叉樹的高度
size_t TreeHeight(TreeNode* root)
{
	if (root == NULL)
		return 0;
	size_t lheight = TreeHeight(root->lchild) + 1;
	size_t rheight = TreeHeight(root->rchild) + 1;
	return rheight > lheight ? rheight : lheight;
}


/////求葉子結點個數
size_t TreeLeafSize(TreeNode* root)
{
	if (root == NULL)
		return 0;
	if (root->lchild == NULL && root->rchild == NULL)
		return 1;
	return TreeLeafSize(root->lchild) + TreeLeafSize(root->rchild);
}

////求K層節點的個數
size_t TreeKLevelSize(TreeNode* root, int K)
{
	if (root == NULL)
		return 0;
	if (K == 1)
		return 1;
	return TreeKLevelSize(root->lchild, K - 1) + TreeKLevelSize(root->rchild, K - 1);
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章