介绍树的概念,二叉树相关结构计算以及堆。

一.树

树结构具有明显的层次特性,如同家族谱系一样。一般用“父子”来称呼体现节点之间的层次差异和继承关系。父节点处于较高层次,是子节点的“先辈”,子节点则依赖父节点存在,如同现实中子女与父母的关系,便于描述和理解树中节点的上下层级关系。

1.概念:

任何一棵树都是由根和子树构成的,子树没有了树就结束了。

二叉树及建堆的详细介绍_数据结构

基本概念总结

①节点的度

 节点拥有子树数目称为节点的度。如,A有3个子节点,那么它的度就是3。

②树的度:

  树中节点的最大度数称为树的度。度为0的节点称为叶子节点,也叫终端节点。

④内部节点/非终端节点

   度不为0的节点称为内部节点。反之度为0称叶节点或终端节点

⑤父节点、子节点和兄弟节点

  一个节点的子树的根节点是该节点的子节点,而这个节点就是其子节点的父节点。具有相同父节点的节点互为兄弟节点。

⑥树的深度/高度

  树中节点的最大层次称为树的深度或。根节点在第1层,若某节点在第i层,那么它的子节点在第i + 1层。

⑦森林

  森林是m(m≥0)棵互不相交的树的集合。可以把树的根节点删去就得到森林。

⑧ 有序树和无序树

  如果树中节点的子树是有顺序的,即子树之间有先后次序之分,则称该树为有序树;否则称为无序树

注意:树的子树是不相交的,除了根节点,每个节点有且只有一个父节点,一棵N个节点的树就有N-1条边。

2.左孩子右兄弟表示法:

左孩子右兄弟表示法是一种用于表示树的数据结构的链式方法,树的每个节点除了存储自身的数据外,还包含两个指针,分别指向该节点的第一个孩子节点(左孩子)和下一个兄弟节点(右兄弟)。通过这种方式,就能够找全树的所有节点。

二叉树及建堆的详细介绍_建堆_02

二.二叉树

1.概念:

(1)二叉树最大度是2  (2)二叉树有左右子树之分,次序不能颠倒(有序树)

简单理解,二叉树是一棵计划生育的树,每个节点最多只能有两个子节点

二叉树及建堆的详细介绍_建堆_03

2.两个特殊二叉树:

(1)满二叉树:每一层都是满的。

二叉树及建堆的详细介绍_二叉树_04

注:在树结构中,根节点所在的层为第0层。从根节点开始,向下延伸一层,根节点的子节点所在的层为第1层。以此类推,每向下延伸一层,层数就加1。(这样设计符合计算机习惯(如数组索引从0开始),便于描述、计算树的性质,实现递归。)

(2)完全二叉树:除了最后一层,每一层都是满的。

二叉树及建堆的详细介绍_建堆_05

二叉树只有度0,度1,度2(1个子树,2个子树,无子树),分别用N0,N1,N2表示。那么在完全二叉树中有一下结论:N1=0或1,N0 = N2+1;

由此,如果已知节点数,就可求N0,N1,N2。

3.parent和child关系:

二叉树及建堆的详细介绍_数据结构_06

那么在顺序存储中如何连接父子关系?

//结论:parent/child为数组下标
parent = (child - 1)/2;
child =parent*2 + 2;
  • 1.
  • 2.
  • 3.

顺序(数组)方式:适合对满二叉树/完全二叉树存储,若不是完全二叉树,数组中会间隔很多“空”出来位置,浪费空间。

三.堆

1.基本概念:

堆是一种特殊的数据结构,通常用于实现优先队列。

- 近似完全二叉树结构:堆可以被看作是一棵近似完全二叉树。除了最底层,其他层的节点都是满的,并且最底层的节点都尽量靠左排列 。

  • 堆序性质

- 最大堆(大根堆、大顶堆) :在大堆中,任意节点的值都大于或等于其子节点的值。这意味着堆顶(根节点)元素是整个堆中的最大值

- 最小堆(小根堆、小顶堆) :在小堆中,任意节点的值都小于或等于其子节点的值。即堆顶元素是整个堆中的最小值

2.建堆:

当我们有10个数据,该如何让它以二叉树的形式进行操作?

二叉树及建堆的详细介绍_堆_07

(1)向下调整建堆:

  即不断调整父子节点顺序,让值大的作为父节点,值小的作为子节点。但如果我们从上往下调整,比如从1开始不断调整父子节点:

二叉树及建堆的详细介绍_堆_08

调整完第三次我们就会发现,值为9的节点并没有被调整到合适的位置?难道我们要再从根节点8调整一遍?每一次调整完都仍需要不断重复调整根节点?

这当然是不正确的。

向下调整建堆的前提条件是“下”部分已经是一个堆,那么就我们需要从最后一棵子树开始调整,让下面的部分先形成堆。

先找到最后一个节点的父节点,再不断向前,直到整棵树的根节点:

二叉树及建堆的详细介绍_数据结构_09

最终结果满足任意子节点值小于父节点:建堆完成。

有了逻辑开始实现代码(仅以int类型10个元素为例):

#include<iostream>
using namespace std;

void Adjustdown(int* arr, int parent)
{
  //根据公式找到子节点下标
	int child = parent * 2 + 1;
  
	while (child < 10)
	{
    //如果有两个子节点,寻找到值更大的下标
    //注意:要先判断child+1与树大小的关系,防止越界访问
		if (child + 1 < 10 && arr[child + 1] > arr[child])
		{
			child = child + 1;
		}
  	//如果子节点更大,交换父子节点的值
		if (arr[parent] < arr[child])
		{
			int tmp = arr[parent];
			arr[parent] = arr[child];
			arr[child] = tmp;
		}
		else
		{
			break;
      //由于每次调整时"下部分树"已经是堆,所以当父节点>子节点时可直接结束
		}
    //更新父子节点,不断向下调整,直到指向叶节点
		parent = child;
		child = parent * 2 + 1;
	}
}

int main()
{
	int arr[] = { 1,6,8,9,7,0,2,3,4,5 };
  //计算最后一个节点对应的父节点
	int parent = (sizeof(arr)/sizeof(arr[0]) - 1 -1) / 2;
  //sizeof(arr)/sizeof(arr[0]) 元素个数-1是最后一个元素下标
  //再-1是公式中的-1: parent = (child-1)/2
	for (int i = parent; i >= 0; i --)
	{
		Adjustdown(arr, i);//依次将子树根节点的下标作为参数
	}
	for (auto e : arr)
	{
		cout << e << " ";
	}
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
(2)向上调整建堆

与向下调整建堆通过将节点向下调整形成堆一致,我们要将节点不断向上调整完成建堆过程。同样要满足:调整时“上部分”已经是堆,那么我们需要从上开始调整。

二叉树及建堆的详细介绍_堆_10

对之后的不满足 父>子 的节点继续操作,直到最后一个节点结束。

二叉树及建堆的详细介绍_建堆_11

如最右侧图所示:通过向上调整建堆得到了一个最大堆。

但是我们观察到,即使这与向下调整建堆得到的堆并不完全一致,但都有任意父节点 > 子节点。由此可以得出:不同建堆方法得到的堆可能不一致,但都它们都是堆(类似导数的原函数不唯一)。

二叉树及建堆的详细介绍_数据结构_12

代码实现:

void AdjustUp(int* arr,int child)
{
	int parent = (child - 1) / 2;
  //注意不能用parent>0作为判断条件
  //	parent = (child - 1) / 2;永远是大于0的
	while (child > 0)
	{
		if (arr[child] > arr[parent])//每个节点只有一个父节点,直接比较即可
		{
			int tmp = arr[child];
			arr[child] = arr[parent];
			arr[parent] = tmp;
		}
		child = parent;
		parent = (child - 1) / 2;
	}
}
int main()
{
	int arr[] = { 1,6,8,9,7,0,2,3,4,5 };
	for(int i =1;i<10;i++)
	{
		AdjustUp(arr, i);
	}
	for (auto e : arr)
	{
		cout << e << " ";
	}
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
总结:

1.调整方向

- 向上调整建堆:从最上的叶子节点开始,将每个节点与其父节点比较交换,直到根节点或满足堆的性质。

- 向下调整建堆:从最下的根节点开始,与它的左右子节点比较交换,不断向下调整,直到叶子节点或满足堆的性质。

2.时间复杂度(向下调整建堆更优)

-向上:O(N*logN)

-向下:O(N)

原因:二叉树最后一层占了几乎一半节点,从下向上调整节点数少,次数少,而从上向下,最后每个节点都要处理,调整次数多(不具体证明了)

3.适用场景

- 向上调整建堆:适合在不断插入元素的过程中构建堆,例如在优先队列中,每次插入一个元素后,可以通过向上调整来维护堆的性质。

- 向下调整建堆:适合在已经有一个无序数组的情况下构建堆,通过从最后一个非叶子节点开始向下调整,可以快速将数组调整为堆。

3.堆的插入删除逻辑:

由于堆的结构,当我们插入删除时需要考虑:删哪个?怎么删?删完怎么调整?怎么调整插入数据?

(1)删除:

尾删不用过多考虑,而头删呢?在数组中直接挪动数据吗?这不可取:效率低下,且整个树的父子关系全乱了。那我们要不断找出子节点,选出大的那个往上挪动?理论可行,但与其这样,我们可以考虑一种更直接的方法:将根节点和最后一个节点交换,,让整个数组大小减1,在通过向下调整建堆将其调整到合适位置(首尾交换,向下调整)

二叉树及建堆的详细介绍_二叉树_13

这样,只需要处理一个元素,就能高效的删除元素,得到新的堆。

(2)插入:

相比较删除,插入简单的多,有了向上调整的逻辑,只需要将数据尾插,之后利用向上调整第一个节点即可。

二叉树及建堆的详细介绍_建堆_14