今日一言:每天给自己一个希望,不为明天烦恼,不为昨天叹息,只为今天更美好;每天给自己一份潇洒,不为明天担忧,不为昨天懊恼,只为今天更快乐!
😋前言
今天我们介绍一个新的概念——树。
1、树的概念及其结构💬
日常生活中,对于数我们可谓熟悉得不能再熟悉了,但数据结构中的树,可不像现实生活中那么直观可见。
树是一种非线性的数据结构,它是由n(n>=0)个有限结点组成一个具有层次关系的集合。把它叫做树是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。
上图所示便是树。
注意:树的每个子节点有且只有一个父节点,不可以有多个父节点!
节点的度:一个节点含有的子树的个数称为该节点的度;
叶节点或终端节点:度为0的节点称为叶节点;
非终端节点或分支节点:度不为0的节点;
双亲节点或父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点;
孩子节点或子节点:一个节点含有的子树的根节点称为该节点的子节点;
兄弟节点:具有相同父节点的节点互称为兄弟节点;
树的度:一棵树中,最大的节点的度称为树的度;
节点的层次:从根开始定义起,根为第1层,根的子节点为第2层,以此类推;
树的高度或深度:树中节点的最大层次;
堂兄弟节点:双亲在同一层的节点互为堂兄弟;
节点的祖先:从根到该节点所经分支上的所有节点;
子孙:以某节点为根的子树中任一节点都称为该节点的子孙。
森林:由m(m>0)棵互不相交的树的集合称为森林
2、二叉树的概念及其结构💬
首先,我们来看看二叉树的逻辑结构。
二叉树,每个父节点最多有两个子节点,可以是2或1或0
二叉树中有满二叉树和完全二叉树。
满二叉树:除最后一层无任何子节点外,每一层上的所有结点都有两个子结点的二叉树。
完全二叉树:二叉树从上往下、从左往右依次填充,节点之间必须要相连续,不能出现空余位置。其实,满二叉树是一种特殊的完全二叉树。
对于以下这种二叉树,就不是完全二叉树了,因为它的最后一层节点并没有连续排布,它只是一个普通的二叉树。
完全二叉树的高度和节点数的关系
对于已知高度为h的二叉树:
二叉树 | 高度 | 总节点数 |
---|---|---|
满二叉树 | h | 2 ^ h - 1 |
完全二叉树 | h | [ 2 ^ (h - 1) , 2 ^ h - 1 ] |
对于已知总结点数为n的二叉树:
二叉树 | 总节点数 | 高度(取整数) |
---|---|---|
满二叉树 | N | log ( N + 1 ) |
完全二叉树 | N | log ( 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个。
有两种解题思路:
-
建堆N个数据的大堆,pop K次就可以了。
但是这个方法有个缺陷:当数据量很大时,比如100亿个整型(大概占37.25G),所需的时间成本和空间成本就太大了。
-
我们取前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;
}