数据结构——二叉树

今日一言:每天给自己一个希望,不为明天烦恼,不为昨天叹息,只为今天更美好;每天给自己一份潇洒,不为明天担忧,不为昨天懊恼,只为今天更快乐!

😋前言

今天我们介绍一个新的概念——树。

1、树的概念及其结构💬

日常生活中,对于数我们可谓熟悉得不能再熟悉了,但数据结构中的树,可不像现实生活中那么直观可见。

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

上图所示便是树。

注意:树的每个子节点有且只有一个父节点,不可以有多个父节点!

节点的度:一个节点含有的子树的个数称为该节点的度;
叶节点或终端节点:度为0的节点称为叶节点;
非终端节点或分支节点:度不为0的节点;
双亲节点或父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点;
孩子节点或子节点:一个节点含有的子树的根节点称为该节点的子节点;
兄弟节点:具有相同父节点的节点互称为兄弟节点;
树的度:一棵树中,最大的节点的度称为树的度;
节点的层次:从根开始定义起,根为第1层,根的子节点为第2层,以此类推;
树的高度或深度:树中节点的最大层次;
堂兄弟节点:双亲在同一层的节点互为堂兄弟;
节点的祖先:从根到该节点所经分支上的所有节点;
子孙:以某节点为根的子树中任一节点都称为该节点的子孙。
森林:由m(m>0)棵互不相交的树的集合称为森林

2、二叉树的概念及其结构💬

首先,我们来看看二叉树的逻辑结构。
二叉树,每个父节点最多有两个子节点,可以是2或1或0

二叉树中有满二叉树和完全二叉树。

满二叉树:除最后一层无任何子节点外,每一层上的所有结点都有两个子结点的二叉树。
完全二叉树:二叉树从上往下、从左往右依次填充,节点之间必须要相连续,不能出现空余位置。其实,满二叉树是一种特殊的完全二叉树。

对于以下这种二叉树,就不是完全二叉树了,因为它的最后一层节点并没有连续排布,它只是一个普通的二叉树。

在这里插入图片描述

完全二叉树的高度和节点数的关系

对于已知高度为h的二叉树:

二叉树高度总节点数
满二叉树h2 ^ h - 1
完全二叉树h[ 2 ^ (h - 1) , 2 ^ h - 1 ]

在这里插入图片描述

对于已知总结点数为n的二叉树:

二叉树总节点数高度(取整数)
满二叉树Nlog ( N + 1 )
完全二叉树Nlog ( N + 1 )

在这里插入图片描述

3、二叉树顺序结构及实现(大根堆)💬

二叉树的物理结构

了解完二叉树的逻辑结构,我们来建造它的物理结构。

大家不妨想一想,逻辑上来看是棵树,那么改用什么样的数据结构来创造出这棵树呢?
有两种方法:顺序结构和链式结构。我们先来看顺序结构。

顺序结构是通过顺序表来实现这棵树。我们将一串数据从上到下、从左到右依次存入一个数组当中。
大家是否有疑问,数组和树有啥关系?其实很简单,它们俩本身确实没有任何关系,但是数组拥有一样东西,下标。下标就是关系。

在这里插入图片描述

根据上面的图示,你是否发现了一个规律呢?

对于下标而言:

Parent=(Child - 1)/ 2
LeftChild=Parent * 2 + 1
RightChild=Parent * 2 + 2

于是,我们仅仅通过父节点与子节点的下标关系,就可以通过顺序表来表示二叉树了!

堆是指一个完全二叉树,一棵树所有的子节点>=或<=其父节点。

在这里插入图片描述

小根堆:树中所有父节点都小于/等于子节点
大根堆:树中所有父节点都大于/等于子节点

将一个二叉树排序成小根堆或大根堆(建堆)有两种方法:向上调整和向下调整。

向上调整

核心思路:

建小堆:从叶子开始,和其父节点比较,若子节点(叶子),则和父节点交换,继续进行比较和交换,直到子节点比父节点,或者子节点成为根节点为止。此时就排好了一个数据的位置,此为一次向上调整。

建大堆:从叶子开始,和其父节点比较,若子节点(叶子),则和父节点交换,继续进行比较和交换,直到子节点比父节点,或者子节点成为根节点为止。此时就排好了一个数据的位置,此为一次向上调整。

下面以建小堆为例:
在这里插入图片描述

向下调整

核心思路:

建小堆:从根节点开始,和其两个子节点中较的那个比较,若这个子节点比父节点(根节点)还,则交换它们位置,继续进行比较和交换,直到较的那个子节点比父节点,或者该节点已经变成叶子为止。此时排好了一个数据的位置,此为一次向下调整。

建大堆:从根节点开始,和其两个子节点中较的那个比较,若这个子节点比父节点(根节点)还,则交换它们位置,继续进行比较和交换,直到较的那个子节点比父节点,或者该节点已经变成叶子为止。此时排好了一个数据的位置,此为一次向下调整。

下面同样以建小堆为例:

在这里插入图片描述

顺序结构实现一个大根堆

Heap.h
#include<stdio.h>
#include<assert.h>
#include<malloc.h>
#include<stdbool.h>

//大堆

typedef int HeapDataType;

//顺序表
typedef struct Heap
{
	HeapDataType* a;
	int size;
	int capacity;
}HP;

void HeapInit(HP* php);

void HeapDestory(HP* php)void HeapPush(HP* php, HeapDataType x);

void HeapPop(HP* php);//删除堆顶,这样就可以做选择

HeapDataType HeapTop(HP* php);

bool HeapEmpty(HP* php);

int HeapSize(HP* php);
Heap.c
#include"Heap.h"
void HeapInit(HP* php)
{
	assert(php);

	HeapDataType* tmp = (HeapDataType*)malloc(sizeof(HeapDataType) * 4);
	if (tmp == NULL)
	{
		perror("malloc fail");
		return;
	}
	php->a = tmp;
	tmp = NULL;
	php->size = 0;
	php->capacity = 4;

}
void HeapDestory(HP* php)
{
	assert(php);
	php->size = 0;
	php->capacity = 0;
	free(php->a);
}
void Swap(HeapDataType* a, int child, int parent)
{
	HeapDataType tmp = a[child];
	a[child] = a[parent];
	a[parent] = tmp;
}
// 向上调整
void AdjustUp(HeapDataType* a, int child)
{
	int parent = (child - 1) / 2;
	while (child > 0)
	{
		if (a[child] > a[parent])
		{
			Swap(a, child, parent);
			child = parent;
			parent = (child - 1) / 2;
		}
		else
		{
			break;
		}
	}
}
void HeapPush(HP* php, HeapDataType x)
{
	assert(php);
	if (php->size == php->capacity)
	{
		// 扩容
		HeapDataType* tmp = (HeapDataType*)realloc(php->a, sizeof(HeapDataType) * php->capacity * 2);
		if (tmp == NULL)
		{
			perror("realloc fail");
			return;
		}
		php->a = tmp;
		tmp = NULL;
		php->capacity *= 2;
	}
	php->a[php->size] = x;
	php->size++;
	AdjustUp(php->a, php->size - 1);
}
// 向下调整
void AdjustDown(HeapDataType* a, int size, int parent)
{
	int child = parent * 2 + 1;
	while (child < size)
	{
		// 判断左右孩子哪个大,大孩子是chlid
		if (child + 1 < size && a[child] < a[child + 1])
		{
			++child;
		}
		// 判断大孩子和父亲哪个大
		if (a[child] > a[parent])
		{
			Swap(a, child, parent);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}
void HeapPop(HP* php)
{
	assert(php);
	assert(!HeapEmpty(php));

	Swap(php->a, 0, php->size - 1);
	php->size--;
	AdjustDown(php->a, php->size, 0);
}
HeapDataType HeapTop(HP* php)
{
	assert(php);
	return php->a[0];
}
bool HeapEmpty(HP* php)
{
	assert(php);
	return php->size == 0;
}
int HeapSize(HP* php)
{
	assert(php);
	return php->size;
}
Test.c
#include"Heap.h"
int main()
{
	HP hp;
	HeapInit(&hp);
	HeapPush(&hp, 4);
	HeapPush(&hp, 18);
	HeapPush(&hp, 42);
	HeapPush(&hp, 12);
	HeapPush(&hp, 2);
	HeapPush(&hp, 3);
	HeapPush(&hp, 19);
	HeapPush(&hp, 123);
	HeapPush(&hp, 145);
	HeapPush(&hp, 178);
	HeapPush(&hp, 19);
	HeapPush(&hp, 15);
	HeapPush(&hp, 1);
	HeapPush(&hp, 12);
	while(!HeapEmpty(&hp))
	{
		printf("%d ", HeapTop(&hp));
		HeapPop(&hp);
	}
	printf("\n");
    HeapDestory(&hp);
	return 0;
}

堆排序

如果已知一个数组a[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10},我们必须要像上面一样,手写一串很长的代码来创建一个堆吗?
其实不用,我们可以对这串数组直接建堆。

建堆

无论建大堆还是建小堆,建堆都有两种方式:向上调整建堆、向下调整建堆。
其中向下调整建堆的效率更高。

建堆的条件

建堆是有前提条件的:
向上调整建堆前提是:前面的位置必须是堆。
向下调整建堆前提是:左右子树是大堆或小堆。先从最后一个叶子的父节点开始调整,然后往前一个,再往前一个…,直到根节点。

建堆的时间复杂度

我们通过下面的图片来理解。

向上调整建堆

在这里插入图片描述
在这里插入图片描述

向下调整建堆

在这里插入图片描述
在这里插入图片描述

排序

对于排序而言,遍历时必不可少的,这就已经是O(N)了,再结合上向下调整(向下调整比向上调整效率高),整个排序的时间复杂度是O(N* logN)。

建堆及堆排序代码示例
// 排升序 -- 建大堆 O(N*logN)
void HeapSort(HeapDataType* a, int n)
{
	// 向上调整建堆O(N*logN)
	for (int i = 1; i < n; ++i)
	{
		AdjustUp(a, i);
	}
	// 向下调整建堆O(N)
	for (int i = (n - 1 - 1) / 2; i > 0; --i)
	{
		AdjustDown(a, n, i);
	}
	// 10 9 6 7 8 1 5 2 4 3

	// 排升序O(N*logN)
	for (int k = n - 1; k > 0; --k)
	{
		// 1.交换
		Swap(a, 0, k);
		// 2.向下调整
		AdjustDown(a, k, 0);
	}
}

这里我们要记住一点:排升序要建大堆,排降序要建小堆

冒泡排序时间复杂度:O(N^2)
堆排序时间复杂度:O(N* logN)
向上调整建堆的时间复杂度:O(N* logN)
向下调整建堆的时间复杂度:O(N)
(本文的所有logN都代表以2为底)

Top-K问题

Top-K:找出N个数里最大的前K个。

有两种解题思路:

  1. 建堆N个数据的大堆,pop K次就可以了。

    但是这个方法有个缺陷:当数据量很大时,比如100亿个整型(大概占37.25G),所需的时间成本和空间成本就太大了。

  2. 我们取前K个数据,建一个小堆,再遍历剩下的数据,遇到比堆顶大的就替换堆顶,然后向下调整。最终得到的这个小堆就是前K个最大的数。
    这个方法十分巧妙,建堆的大小只有k个数据,不存在空间成本的问题,另外, 向下调整的时间复杂度是O(N),比建堆的时间复杂度O(N* logN)小很多。

4、二叉树链式结构及实现💬

在顺序结构中,我们涉及到的二叉树都是完全二叉树,而对于非完全二叉树(不连续、有空位),就需要链式结构了。

但是普通的二叉树是没有特别大的意义,我们一般讨论搜索二叉树。
搜索二叉树(排序二叉树):任何一棵树,其左子节点一定比根节点小,右子节点一定比根节点大。

题外话: 数据结构中几个常见的搜索(行业中90%以上的搜索):平衡搜索二叉树、哈希表、红黑树、B树

二叉树的遍历

在这里插入图片描述

前序遍历(前根遍历)

顺序:根 -> 左子树 -> 右子树
1 2 3 Ø Ø Ø 4 5 Ø Ø 6 Ø Ø

// 前序遍历
void PreOrder(BTNode* root)
{
	if (root == NULL)
	{
		printf("NULL ");
		return;
	}

	printf("%d ", root->data);
	PreOrder(root->left);
	PreOrder(root->right);
}

中序遍历(中根遍历)

顺序:左子树 -> 根 -> 右子树
Ø 3 Ø 2 Ø 1 Ø 5 Ø 4 Ø 6 Ø

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

后序遍历

顺序:左子树 -> 右子树 -> 根
Ø Ø 3 Ø 2 Ø Ø 5 Ø Ø 6 4 1

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

层序遍历

1 2 4 3 5 6

前序、中序、后序的代码是通过递归来实现的,而层序遍历需要使用队列来实现。

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

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


// 层序遍历
// 以前我们在队列里存的是一个char/int,现在我们需要存一个节点的指针
typedef BTNode* QDataType;

typedef struct QueueNode
{
	QDataType data;
	struct QueueNode* next;
}QNode;

typedef struct Queue
{
	QNode* head;
	QNode* tail;
}Queue;


void QInit(Queue* ps);
void QDestory(Queue* ps);

void QPush(Queue* ps, QDataType x);
void QPop(Queue* ps);

bool QEmpty(Queue* ps);
int QSize(Queue* ps);

QDataType QFront(Queue* ps);
QDataType QBack(Queue* ps);
Queue.c
#include"Queue.h"

void QInit(Queue* ps)
{
	assert(ps);
	ps->head = ps->tail = NULL;
}
void QDestory(Queue* ps)
{
	assert(ps);
	for (QNode* cur = ps->head; cur <= ps->tail;)
	{
		QNode* next = cur->next;
		free(cur);
		cur = next;
	}
	ps->head = ps->tail = NULL;
}

void QPush(Queue* ps, QDataType x)
{
	assert(ps);
	QNode* newnode = (QNode*)malloc(sizeof(QNode));
	if (newnode == NULL)
	{
		perror("malloc fail");
		return;
	}
	newnode->data = x;
	newnode->next = NULL;
	if (ps->head == NULL)
	{
		ps->head = ps->tail = newnode;
	}
	else
	{
		ps->tail->next = newnode;
		ps->tail = newnode;
	}
}
void QPop(Queue* ps)
{
	assert(ps);
	QNode* newhead = ps->head->next;
	free(ps->head);
	ps->head = newhead;
}

bool QEmpty(Queue* ps)
{
	assert(ps);
	return ps->head == NULL;
}
int QSize(Queue* ps)
{
	assert(ps);
	if (ps->head == NULL)
		return 0;
	if (ps->head == ps->tail)
		return 1;
	int count = 0;
	QNode* cur = ps->head;
	while (cur != ps->tail)
	{
		count++;
		cur = cur->next;
	}
	return count + 1;
}

QDataType QFront(Queue* ps)
{
	assert(ps);
	assert(!QEmpty(ps));
	return ps->head->data;
}
QDataType QBack(Queue* ps)
{
	assert(ps);
	assert(!QEmpty(ps));
	return ps->tail->data;
}
Test.c
#include"Queue.h"

BTNode* BuyNode(BTDataType x)
{
	BTNode* node = (BTNode*)malloc(sizeof(BTNode));
	if (node == NULL)
	{
		perror("malloc fail");
		return NULL;
	}
	node->data = x;
	node->left = NULL;
	node->right = NULL;
	return node;
}

BTNode* CreatTree()
{
	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 = node4;
	node2->left = node3;
	node4->left = node5;
	node4->right = node6;
	node3->right = node7;
	return node1;
}

void LevelOrder(BTNode* root)
{
	Queue q;
	QInit(&q);
	if (root)
		QPush(&q, root);
	while (!QEmpty(&q))
	{
		BTNode* front = QFront(&q);
		QPop(&q);
		printf("%d ", front->data);
		if (front->left)
			QPush(&q, front->left);
		if (front->right)
			QPush(&q, front->right);
	}

	QDestory(&q);
}

int main()
{
	struct TreeNode* root = CreatTree();
	LevelOrder(root);
}

二叉树相关求解

求总节点数

// 遍历
void TreeSize1(BTNode* root, int* psize)
{
	if (root == NULL)
		return;

	++(*psize);
	TreeSize1(root->left, psize);
	TreeSize1(root->right, psize);
}
// 递归 分制
int TreeSize2(BTNode* root)
{
	return root == NULL ? 0 : 
		TreeSize2(root->left) 
		+ TreeSize2(root->right)
		+ 1;
}

求高度

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

	int LeftHeight = TreeHeight(root->left);
	int RightHeight = TreeHeight(root->right);
	return LeftHeight > RightHeight ? LeftHeight + 1: RightHeight + 1;
}

求第k层节点个数

int TreeKLevel(BTNode* root, int k)
{
	assert(k > 0);
	if (root == NULL)
		return 0;
	if (k == 1)
		return 1; 
	return TreeKLevel(root->left, k - 1) + TreeKLevel(root->right, k - 1);

}

求值为x的节点

// 二叉树查找值为x的节点
BTNode* BinaryTreeFind(BTNode* root,BTDataType x)
{
	if (root == NULL)
		return NULL;
	// 根节点
	if (root->data == x)
		return root;
	// 左子树
	BTNode* TreeLeft = BinaryTreeFind(root->left, x);
	if (TreeLeft != NULL)
		return TreeLeft;
	// 右子树
	BTNode*TreeRight = BinaryTreeFind(root->right, x);
	if (TreeRight != NULL)
		return TreeRight;
	return NULL;
}
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
1. 什么是二叉树? 二叉树是一种树形结构,其中每个节点最多有两个子节点。一个节点的左子节点比该节点小,右子节点比该节点大。二叉树通常用于搜索和排序。 2. 二叉树的遍历方法有哪些? 二叉树的遍历方法包括前序遍历、中序遍历和后序遍历。前序遍历是从根节点开始遍历,先访问根节点,再访问左子树,最后访问右子树。中序遍历是从根节点开始遍历,先访问左子树,再访问根节点,最后访问右子树。后序遍历是从根节点开始遍历,先访问左子树,再访问右子树,最后访问根节点。 3. 二叉树的查找方法有哪些? 二叉树的查找方法包括递归查找和非递归查找。递归查找是从根节点开始查找,如果当前节点的值等于要查找的值,则返回当前节点。如果要查找的值比当前节点小,则继续在左子树中查找;如果要查找的值比当前节点大,则继续在右子树中查找。非递归查找可以使用栈或队列实现,从根节点开始,每次将当前节点的左右子节点入栈/队列,直到找到要查找的值或者栈/队列为空。 4. 二叉树的插入与删除操作如何实现? 二叉树的插入操作是将要插入的节点与当前节点的值进行比较,如果小于当前节点的值,则继续在左子树中插入;如果大于当前节点的值,则继续在右子树中插入。当找到一个空节点时,就将要插入的节点作为该空节点的子节点。删除操作需要分为三种情况:删除叶子节点、删除只有一个子节点的节点和删除有两个子节点的节点。删除叶子节点很简单,只需要将其父节点的对应子节点置为空即可。删除只有一个子节点的节点,需要将其子节点替换为该节点的位置。删除有两个子节点的节点,则可以找到该节点的后继节点(即右子树中最小的节点),将其替换为该节点,然后删除后继节点。 5. 什么是平衡二叉树? 平衡二叉树是一种特殊的二叉树,它保证左右子树的高度差不超过1。这种平衡可以确保二叉树的查找、插入和删除操作的时间复杂度都是O(logn)。常见的平衡二叉树包括红黑树和AVL树。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值