数据结构初阶--二叉树介绍(基本性质+堆实现顺序结构)

时间:2022-11-28 15:10:38

树的相关概念

数据结构初阶--二叉树介绍(基本性质+堆实现顺序结构)

节点的度:一个节点含有的子树的个数称为该节点的度; 如上图: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;
}