数据结构--二叉树

1. 树的概念及其结构

1.1 树的概念

树是一种非线性的数据结构,它是由n(n>=0)个有限结点组成一个具有层次关系的集合。把它叫做树是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。

  •  有一个特殊的结点,称为根节点,根节点没有前驱节点
  • 除根节点外,其余结点被分成M(M>0)个互不相交的集合T1、T2、……、Tm,其中每一个集合Ti(1<= i<= m)又是一棵结构与树类似的子树。每棵子树的根结点有且只有一个前驱,可以有0个或多个后继结点。
  • 树形结构中,子树不能有交集,否则就不是树形结构。

1.2 树的相关概念

节点的度:一个节点含有的子树的个数称为该节点的度; 


叶节点或终端节点:度为0的节点称为叶节点;


非终端节点或分支节点:度不为0的节点;


双亲节点或父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点; 


孩子节点或子节点:一个节点含有的子树的根节点称为该节点的子节点;


兄弟节点:具有相同父节点的节点互称为兄弟节点;


树的度:一棵树中,最大的节点的度称为树的度; 


节点的层次:从根开始定义起,根为第1层,根的子节点为第2层,以此类推;


树的高度或深度:树中节点的最大层次。
 

1.3 树的表示

树结构的表示方式相对于线性表存储起来是比较麻烦的,既要保存数据,又要保存结点与结点之间的关系。表示树有很多种方式,在这里就用孩子兄弟表示法

typedef int DataType
struct Node
{
    DataType val;                //数据
    struct Node* firstChild1;    //第一个孩子结点
    struct Node* pNextBrother;   //指向下一个兄弟结点
};
    

 //树在实际中的可以表示文件系统的目录树结构。

2. 二叉树的概念及其结构

2.1 概念

一棵二叉树是结点的一个有限集合,该集合要么为空;要么就由一个根节点加上其左子树和右子树的二叉树组成。

  •  二叉树中不存在度大于2的结点;
  • 二叉树中的子树有左右之分·,次序不能颠倒,因此二叉树是有序树。
  • 任意二叉树都由以下几种情况复合而成:

2.2 特殊的二叉树

  1. 满二叉树:一个二叉树,如果每一层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为k,且总结点数为2^k-1,则它就是满二叉树;
  2. 完全二叉树:完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为K的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时称之为完全二叉树。 要注意的是满二叉树是一种特殊的完全二叉树。

2.3 二叉树的性质

1. 若规定根节点的层数为1,则一棵非空二叉树的第i层上最多有2^(i-1)个结点.
2. 若规定根节点的层数为1,则深度为h的二叉树的最大结点数是2^h - 1.
3. 对任何一棵二叉树, 如果度为0其叶结点个数为N0 , 度为2的分支结点个数为N2,则有N0=N2+1.
4. 若规定根节点的层数为1,具有n个结点的满二叉树的深度,h= log(n+1)(ps: 是log以2为底,n+1为对数)
5. 对于具有n个结点的完全二叉树,如果按照从上至下从左至右的数组顺序对所有节点从0开始编号,则对于序号为i的结点有:

已知父节点序号,那么左子树序号为:parent*2+1;右子树序号为parent*2+2;已知左右任意子树,那么父节点序号为(child-1)/2(因为计算机计算会取整)。

  • 若i>0,i位置节点的双亲序号:(i-1)/2;i=0,i为根节点编号,无双亲节点;
  •  若2i+1<n,左孩子序号:2i+1,2i+1>=n否则无左孩子;
  • 3若2i+2<n,右孩子序号:2i+2,2i+2>=n否则无右孩子

2.4 存储结构

二叉树一般可以使用两种结构存储,一种顺序结构,一种链式结构。

2.4.1 顺序存储

顺序结构存储就是使用数组来存储,一般使用数组只适合表示完全二叉树,因为不是完全二叉树会有空间的浪费,而现实中,堆会用数组来存储。二叉树顺序存储在物理上是一个数组,在逻辑上是一颗二叉树。

2.4.2 链式存储 

3.二叉树的顺序结构及其实现

3.1 二叉树的顺序结构

普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。而完全二叉树更适合使用顺序结构存储。现实中我们通常把堆(一种二叉树)使用顺序结构的数组来存储,需要注意的是这里的堆和操作系统虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段。

3.2 堆的概念及其结构

堆是特殊的完全二叉树结构,堆可以分为大堆小堆大堆的子节点不大于父节点;而小堆的子节点不小于父节点。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆

3.3 堆的实现

3.3.1 列出接口

#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>

typedef int HPDataType;
typedef struct Heap
{
	HPDataType* a;
	int size;
	int capacity;
}HP;

void Swap(HPDataType* a, HPDataType* b);
void InitHeap(HP* php);//初始化
void HeapDestory(HP* php);//销毁堆
void AdjustUp(HPDataType* a, HPDataType child);//向上调整算法
void HeapPush(HP* php, HPDataType x);//向堆插入数据
void AdjustDown(HPDataType* a, int size, int root);//向下调整算法
void HeapPop(HP* php);//删除根
void HeapPrint(HP* php, size_t size);//打印二叉树中的数据

 3.3.2 实现接口

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

void InitHeap(HP* php)
{
	assert(php);

	php->a = NULL;
	php->size = php->capacity = 0;
}

void HeapDestory(HP* php)
{
	assert(php);

	free(php->a);
	php->a = NULL;
	php->size = php->capacity = 0;
}

void HeapPrint(HP* php, size_t size)
{
	assert(php);

	size_t cur = 0;
	while (cur < size)
	{
		printf("%d ",php->a[cur]);
		cur++;
	}
}

插入函数 

void AdjustUp(HPDataType* a,HPDataType child)
{
	size_t 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 HeapPush(HP* php, HPDataType x)
{
	assert(php);

	if (php->size == php->capacity)
	{
		int newcapacity = php->capacity == 0 ? 4 : php->capacity * 2;

		HPDataType* tmp = realloc(php->a, sizeof(HPDataType) * newcapacity);
		assert(tmp);
		php->a = tmp;
		php->capacity = newcapacity;
	}
	php->size++;
	php->a[php->size - 1] = x;


	AdjustUp(php->a,php->size-1);
}

 向二叉树中插入数据都是插入在物理结构的最后的,也就是数组最后的位置。类似于向顺序表中插入数据,唯一不同的一点是插入数据后仍然要保持大(小)堆结构,这就需要向上依次调整。

 删除函数

void AdjustDown(HPDataType* a, int size, int root)
{
	int parent = root;
	int child = root * 2 + 1;
	while (child < size)        //向下进行迭代的条件
	{
		//假如右子树小,就用右子树
		if (child + 1 < size && a[child] > a[child + 1])
		{
			child++;
		}

		if (a[parent] > a[child])
		{
			Swap(&a[parent], &a[child]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}

	}
}

void HeapPop(HP* php)
{
	assert(php);
	assert(php->size > 0);

	//交换头尾
	Swap(&php->a[0], &php->a[php->size - 1]);
	
	php->size--;

	AdjustDown(php->a,php->size,0);
}

 删除堆中的数据,就是删除树根!而直接删除树根后,如果不加调整是会造成结构混乱的,而且直接删除根也是不容易操作的!所以使用一种新的方法,第一步是,将堆的根和堆的最后一个数据交换位置,然后size--;第二步是,向下调整结构,因为最后面的数据到根部肯定是会导致堆的结构变化的使用向下调整法要保证左右子树为小(大)堆。

3.3.3 创建堆 

下面我们给出一个数组,这个数组逻辑上可以看做一颗完全二叉树,但是还不是一个堆,现在我通过算法,把它构建成一个堆。根节点左右子树不是堆,我们怎么调整呢?这里我们既可以用向下调整法进行调整建堆,又可以用向上调整法调整建堆。
 

int a[] = {1,5,3,8,7,6},将其调整为大堆。

向上调整法建堆:

 向上调整法就是在数组的空间内进行调整,按照数组内数字的顺序依次向上调整

int i = 0;
for (i = 1; i < n; i++)
{
	AdjustUp(a, i);
}

//n为数组内数字的个数

 向下调整法建堆:

for (i = (n-1-1)/2; i >= 0; i--)
{
	AdjustDown(a, n,i);
}
//第一个非叶子结点序号为(n-1-1)/2.

 3.3.4 建堆的时间复杂度

用向上调整法进行建堆的时间复杂度很显然为O(N*logN),因为向上调整法复杂度为O(logN),为堆的深度。进行了N次就是N*logN。

而用向下调整法进行建堆的时间复杂度为O(N):

3.4 堆的应用

3.4.1 用堆来实现排序

如果是升序就建小堆,如果是降序就建大堆。利用堆删除的思想进行排序。

1.利用HeapPush()将数组中的数据放到一个堆当中;

2.然后将堆根中的数据赋值给数组中的第一个数,之后再用HeapPop()删除根的数据,之后重复上述的步骤,直到堆空;(因为在向堆插入数据时,只能保证结构为大小堆结构,并不能保证物理结构上的按降升序排列的,但是能保证根上为最大或者最小的数据,所以再用删除数据的操作,这样能保证删除后的根仍然为堆中最大或最小的数据)

3.用堆实现排序还需要新建三个函数:

bool HeapEmpty(HP* php);判断堆是否为空
void HeapSize(HP* php);得到堆中数据个数
int HeapTop(HP* php);得到根的数据

#include"heap.h"

bool HeapEmpty(HP* php)
{
	assert(php);
	return php->size == 0;
}

void HeapSize(HP* php)
{
	assert(php);
	return php->size;
}

int HeapTop(HP* php)
{
	assert(php);
	return php->a[0];
}

void HeapSort(int* a, size_t size)
{
	HP hp;
	InitHeap(&hp);
	int i = 0;
	for (i = 0; i < size; i++)
	{
		HeapPush(&hp, a[i]);
	}

	int j = 0;
	for (j = 0; j < size; j++)
	{
		a[j] = HeapTop(&hp);
		HeapPop(&hp);
	}
}

int main()
{
	int a[] = { 9,8,7,6,5,4,3,1,2 };
	size_t size = sizeof(a) / sizeof(int);
	HeapSort(a, size);

	int i = 0;
	for (i = 0; i < size; i++)
	{
		printf("%d ", a[i]);
	}

	return 0;
}

这种排序的方法时间复杂度为O(N*logN),相比较于之前的冒泡排序的O(N*N)有了大大地提升。 但是仔细想想,以后每次堆排序是不是都要把这些函数写出来然后再进行排序?这是非常麻烦的,而且在排序的时候还在数组外额外地建立了一个堆,使得空间复杂度为O(N)!现在我们来进行优化一下使得空间复杂度为O(1)并且使得排序的步骤没有这么麻烦和复杂。

3.4.2 堆排序

把数组调整成为堆,就不用创建新堆。在原来数组的基础上进行排序,可以将空间复杂度降到O(1),要把数组调整为升序的,就要建大堆。

  1. 先把数组调整为大堆(用向下调整法)
  2. 在进行如下图的一系列交换;

void HeapSort(int* a,int n)
{
	/*建堆--向下调整*/
	for (i = (n-1-1)/2; i >= 0; i--)
	{
		AdjustDown(a, n,i);
	}

	int end = n - 1;
	while (end > 0)
	{
		Swap(&a[0], &a[end]);
		AdjustDown(a, end, 0);
		end--;
	}
}

int main()
{
	int a[] = { 9,8,7,6,5,4,3,1,2 };
	size_t size = sizeof(a) / sizeof(int);
	HeapSort(a, size);

	size_t i = 0;
	for (i = 0; i < size; i++)
	{
		printf("%d ", a[i]);
	}

	return 0;
}

3.4.3 TOP-K问题 

TOP-K问题:即求数据结合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。当样本数量比较小时还可以进行排序;但是当样本数量很大时,比如全球人数等等,数据一下子不能加入到内存中,这时就用到堆的思想。

1. 用数据集合中前K个元素来建堆:求前k个最大的元素,则建小堆;求前k个最小的元素,则建大堆。

2. 用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素。将剩余N-K个元素依次与堆顶元素比完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素。

void PrintTopK(int k, int* a, int n)
{
	//建小堆
	int* minheap = (int*)malloc(sizeof(int) * k);
	assert(minheap);
	
	for (int i = 0; i < k; i++)
	{
		minheap[i] = a[i];
	}

	for (int i = (k - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(minheap, k, i);
	}

	//比较大小
	for (int i = k; i < n; i++)
	{
		if (minheap[0] < a[i])
		{
			minheap[0] = a[i];
			AdjustDown(minheap, k, 0);
		}
	}

	for (int i = 0; i < k; i++)
	{
		printf("%d ", minheap[i]);
	}
}

void TestTopk()
{
	int* a = (int*)malloc(sizeof(int) * 10000);
	assert(a);
	srand(time(0));

	for (int i = 0; i < 10000; i++)
	{
		a[i] = rand() % 10000;
	}
	a[2] = 11111;
	a[1223] = 12354;
	a[365] = 24568;
	a[83] = 58934;
	a[6357] = 12348;
	a[4368] = 36897;

	PrintTopK(6, a, 10000);
}

4.二叉树链式结构的实现

4.1 前言

为了在后面分析的更简便,在这里先写一个二叉树(并不是正式的创建方式)

typedef int BTDataType;
typedef struct BinaryTreeNode
{
    BTDataType data;
    struct BinaryTreeNode* left;
    struct BinaryTreeNode* right;
}BTNode;

BTNode* BuyNode(BTDataType x)
{
    BTNode* node = (BTNode*)malloc(sizeof(BTNode));
    assert(node);

    node->data = x;
    node->left = node->right = NULL;
    return node;
}

BTNode* CreatBinaryTree()
{
    BTNode* node1 = BuyNode(1);
    BTNode* node2 = BuyNode(2);
    BTNode* node3 = BuyNode(3);
    BTNode * node4 = BuyNode(4);
    BTNode* node5 = BuyNode(5);
    BTNode* node6 = BuyNode(6);
    BTNode* node7 = BuyNode(7);
    node1->left = node2;
    node1->right = node3;
    node2->left = node4;
    node2->right = node5;
    node3->left = node6;
    node3->right = node7;

    return node1;
}

4.2 二叉树的遍历

二叉树的遍历可以说是最基础的一项操作了,是很重要的一项运算之一,也是二叉树上其他运算的基础。二叉树的遍历有前序/中序/后序/的递归结构遍历。

4.2.1 前序遍历

访问根结点的操作发生在遍历其左右子树之前。

void PreOrder(BTNode* root)
{
    if (root == NULL)
    {
        printf("NULL ");
        return; 
    }
    printf("%d ", root->data);
    PreOrder(root->left);
    PreOrder(root->right);
}

4.2.2 中序遍历

访问根结点的操作发生在遍历其左右子树之中(间)。

void InOrder(BTNode* root)
{
    if (root == NULL)
    {
        printf("NULL ");
        return;
    }
    InOrder(root->left);
    printf("%d ", root->data);
    InOrder(root->right);
}

4.2.3 后序遍历

访问根结点的操作发生在遍历其左右子树之后。

void PostOrder(BTNode* root)
{
    if (root == NULL)
    {
        printf("NULL ");
        return;
    }
    PostOrder(root->left);
    PostOrder(root->right);
    printf("%d ", root->data);
}

4.2.4 层序遍历

层序遍历:除了先序遍历、中序遍历、后序遍历外,还可以对二叉树进行层序遍历。设二叉树的根节点所在层数为1,层序遍历就是从所在二叉树的根节点出发,首先访问第一层的树根节点,然后从左到右访问第2层上的节点,接着是第三层的节点,以此类推,自上而下,自左至右逐层访问树的结点的过程就是层序遍历。层序遍历不需要用递归实现,用队列实现。

 先建一个队列,创建一个循环,先把根结点放进去,然后再删除队列中的一个元素,并打印这个元素,如果被删除的元素的子树结点不为空,就把子树结点放进去。判断一下队列是否为空,如果不为空就再删除队列中的一个元素,并打印这个元素,如果被删除的元素的子树结点不为空,就把子树结点放进去。如此反复循环,直到为空为止。这时层序遍历完成。

4.3 计算二叉树中的结点个数

4.3.1 遍历+计数

//遍历+计数
//第1种
int GetCount(BTNode* root)
{
    static int count = 0;
    if (root == NULL)
    {
        return 0;
    }

    count++;
    GetCount(root->left);
    GetCount(root->right);

    return count;
}
//在函数中创建静态局部变量,它必须要返回才能拿到count,
//而且连续调用时,这种方法会产生累加的效应,不推荐这种方法

//第2种
int count = 0;
void GetCount(BTNode* root)
{
    if (root == NULL)
    {
        return;
    }
    count++;
    GetCount(root->left);
    GetCount(root->right);

}
//创建全局变量count可以解决掉第一种的弊端,在每次调用函数之前
//将count置为0即可解决问题,但是还是有弊端

//第3种
void GetCount(BTNode* root, int* pcount)
{
    if (root == NULL)
    {
        return 0;
    }

    (*pcount)++;
    GetCount(root->left,pcount);
    GetCount(root->right,pcount);
}
//通过传地址的方式更实用!

4.3.2 分治

//第二种 分治
int GetCount(BTNode* root)
{
    return root == NULL ? 0 : GetCount(root->left) + GetCount(root->right) + 1;
}

//左子树中的结点个数+右子树中的结点个数+根节点自己

利用递归的思想对二叉树进行分治,把复杂的问题分成更小规模的子问题,子问题再分成更小规模的子问题....直到子问题不可再分割,就可以出结果。 

4.4 求第k层的结点个数

int BTreeLevelSize(BTNode* root,int k)
{
    assert(k >= 1);

    if (k == 1)
    {
        return 1;
    }

    return BTreeLevelSize(root->left, k - 1) + BTreeLevelSize(root->right, k - 1);
}
//返回下一层的k-1层的左右子树结点数之和

 求第k层的数,也可以用分治的思想,对于第一层来说是求第k层的结点数;对于第二层来说是求第k-1层的结点数;对于第三层来说是求第k-2层的节点数........直到第k层,这时候结点开始返回个数。写代码时需要返回左右子树的k-x层的个数之和。

4.5 求叶子结点个数

int BTreeLeafSize(BTNode* root)
{
    if (root == NULL)
        return 0;

    if (root->left == NULL && root->right == NULL)
        return 1;

    return BTreeLeafSize(root->left)+BTreeLeafSize(root->right);
}

 类似求第k层结点个数,不过要改变最后的返回判断条件,就是判断为叶子结点的条件。

4.6 求二叉树的高度

int BTreeDepth(BTNode* root)
{
    if (root == NULL)
    {
        return 0;
    }

    int LeftDepth = BTreeDepth(root->left);
    int rightDepth = BTreeDepth(root->right);
    return LeftDepth > rightDepth ? LeftDepth + 1 : rightDepth + 1;
}

 继续利用分治的思想,只是判断左右子树哪个层数多,多的层数加上根节点这一层就是二叉树的高度。向下递归,直到节点为空,这时就是最后一层+1,开始向上返回值。

4.7 在二叉树中找寻某个数

BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{
    if (root == NULL)
        return NULL;
    
    if (root->data == x)
        return root;

    BTNode* node = BinaryTreeFind(root->left, x);
    if (node != NULL)
        return node;

    node = BinaryTreeFind(root->right, x);
    if (node != NULL)
        return node;

    return NULL;
}

先从左子树中找,如果找到目标直接返回;如果没有找到,再去右子树中找。找得到才返回root,停止寻找;如果找不到就是一直向下找,直到返回空。如果一直找不到最后就返回空。

4.8 判断树是否为完全二叉树

判断一个树是不是完全二叉树,首先要知道完全二叉树是什么样的。完全二叉树是的结点从上到下是连续的,而不完全的二叉树的结点是不连续的。可以将层序遍历进行改造,判断是否为完全二叉树。

//判断树否为完全二叉树
bool Judege(BTNode* root)
{
    Queue q;
    QueueInit(&q);

    if (root)
        QueuePush(&q, root);

    while (!QueueEmpty(&q))
    {
        BTNode* front = QueueFront(&q);

        if (front == NULL)
            break;

        QueuePop(&q);

        QueuePush(&q, front->left);
        QueuePush(&q, front->right);
    }

    while (!QueueEmpty(&q))
    {
        BTNode* front = QueueFront(&q);

        if (front != NULL)
            return false;
        QueuePop(&q);
    }

    QueueDestory(&q);

    return true;
}

 在往队列中插入数据时,不管子树结点是不是NULL都插入进去,当删除元素删除到NULL时,退出循环;重新创建一个循环,对NULL后面的元素进行遍历,如果发现有不为NULL的元素,那么就不是完全二叉树;当遍历到队列为空后,都为NULL,就是完全二叉树。

4.9 创建一个二叉树 

通过前序遍历的数组"ABD##E#H##CF##G##"构建二叉树。

BTNode* CreatBTree(char* a,int *pi)
{
    if (a[*pi] == '#')
    {
        (*pi)++;
        return NULL;
    }

    BTNode* node = (char*)malloc(sizeof(BTNode));
    assert(node);

    node->data = a[(*pi)++];
    node->left = CreatBTree(a, pi);
    node->right = CreatBTree(a, pi);

    return node;
}

依然使用分治的思想,因为是前序遍历的顺序,所以先把左子树填满,当遇到NULL时,再处理右子树。

数组的二叉树图:

 函数的调用栈帧图:

  • 25
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 21
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 21
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

_w_z_j_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值