一.二叉树
1.什么是树?
树是一种非线性的数据结构,它是由n个节点组成的一个具有层次关系的集合,它可以分为根节点和子树。例如:A是根节点,B以下的树是左子树,C以下的树是右子树,左子树和右子树也可以分成根节点和子树。
2.树的概念
节点的度:一个节点所含子树的个数是该节点的度;如上图:B的度是3
叶节点:度为0的节点;如上图:K,J,L,O,P都是叶子节点
分支节点:度不为0的节点
父节点:若一个节点有子节点,则这个节点就是子节点的父节点;如上图:A是B的父节点
子节点:一个节点具有子树的根节点,根节点称为该节点的子节点,如上图:B是A的子节点
兄弟节点:具有相同的父节点的节点为兄弟节点;如上图:D,E,F是兄弟节点
树的度:一颗树,所有节点中最大的度是树的度;如上图:树的度是3
节点的层:从根开始是第一层,根的子节点是第二层
树的高度:树中节点的最大层,如上图:树的高度是5
3.二叉树的概念
二叉树的度是二,所有节点最大的度不能超过二,节点的度可以是0也就是空树。
例如:A是根节点,B以下的树是左子树,C以下的树是右子树;左子树也可以分成根节点和子树,B是根节点,D以下的树是左子树,E以下的树是右子树;右子树也可以分成根节点和子树,C是根节点,F以下的树是左子树,G以下的树是右子树
4.特殊的二叉树
- 满二叉树: 一个二叉树,如果每一个层的结点数都达到最大数,这个树就是满二叉树。如果满二叉树的层数是k,那么节点数就是2^k-1(可以用等比数列公式计算,首项a1=1,等比q=2,项数n=k)
- 完全二叉树:假如一个满二叉树的层数是n,第n层的节点可以不满,但是每个节点要挨着。满二叉树是一种特殊的完全二叉树
5.二叉树的实现
1.顺序存储
顺序存储的前提是完全二叉树,顺序存储就是使用数组来存储,类似顺序表的结构,因为完全二叉树的每个节点都是紧紧挨着,不会有空间的浪费,而且会有特殊的性质。二叉树顺序存储在物理上是一个数组,在逻辑上是一棵二叉树。
parent为父节点,leftchild是父节点的左边子节点,rightchild是父节点的右边子节点
特殊性质:leftchild = parent * 2 + 1
rightchild = parent * 2 + 2
parent = (child - 1) / 2 //child可以是leftchild或者rightchild
根据这个性质:知道parent的下标可以推算出leftchild和rightchild的下标,知道leftchild或者rightchild的下标可以推算出parent的下标,可以直接找到父节点和子节点的下标,但是前提这棵树是完全二叉树
2.链式存储
二叉树的链式实现是指,用链表表示二叉树,其中链表的节点由数据域和左右指针域,数据域用于存储数据,指针域用于存储左右子节点的地址
typedef char BTDataType;
typedef struct BinaryTreeNode
{
struct BinaryTreeNode* left;
struct BinaryTreeNode* right;
BTDataType val;
}BTNode;
typedef BTNode* QlistData;
typedef struct QlistNode
{
QlistData Data;
struct QlistNode* next;
}QlistNode;
//每一个节点的类型
void QlistInit(Queue* pc)
//队列初始化
{
assert(pc);
pc->front = pc->rear = NULL;
pc->size = 0;
}
void QlistPush(Queue* pc, QlistData x)
//入队列
{
assert(pc);
//这里-----------------------------------------------------
QlistNode* NewNode = (QlistNode*)malloc(sizeof(QlistNode));
if (NewNode == NULL)
{
perror("QlistPush");
exit(-1);
}
NewNode->Data = x;
NewNode->next = NULL;
//从上到下其实是QlistBuy创建一个新的节点-------------------
if (pc->rear == NULL)//此时队列为空
{
pc->rear = pc->front = NewNode;
}
else
{
pc->rear->next = NewNode;
pc->rear = pc->rear->next;
}
++pc->size;
}
void QlistPop(Queue* pc)
//出队列
{
assert(pc);
assert(!QlistEmpty(pc));
if (pc->rear == pc->front)
{
free(pc->front);
pc->rear = pc->front = NULL;
}
else
{
QlistNode* cur = pc->front;
pc->front = pc->front->next;
free(cur);
cur = NULL;
}
--pc->size;
}
bool QlistEmpty(Queue* pc)
//判断队列是否为空
{
assert(pc);
return pc->front == NULL;
}
QlistData QlistFront(Queue* pc)
//获取队首元素
{
assert(pc);
assert(!QlistEmpty(pc));
return pc->front->Data;
}
QlistData QlistBack(Queue* pc)
//获取队尾元素
{
assert(pc);
assert(!QlistEmpty(pc));
return pc->rear->Data;
}
int QlistSize(Queue* pc)
//获取有效元素的个数
{
assert(pc);
return pc->size;
}
void QlistDestory(Queue* pc)
//销毁节点
{
assert(pc);
QlistNode* cur = pc->front;
while (pc->front)
{
pc->front = pc->front->next;
free(cur);
cur = pc->front;
}
//空间释放后指针置空
pc->front = pc->rear = NULL;
pc->size = 0;
}
void QlistPrint(Queue* pc)
//打印队列
{
assert(pc);
QlistNode* cur = pc->front;
while (cur)
{
printf("%d ", cur->Data->val);
cur = cur->next;
}
printf("\n");
}
//创建一个节点
BTNode* BuyBinaryTree(int x)
{
BTNode* NewNode = (BTNode*)malloc(sizeof(BTNode));
NewNode->left = NULL;
NewNode->right = NULL;
NewNode->val = x;
}
6.二叉树节点和高度问题
二叉树这类的问题可以用递归快速解决,因为二叉树的特殊性质,二叉树可以分成根节点和左右子树,左右子树也可以分成根节点和左右子树,这个过程和递归相似,所以使用递归解决很合适
递归重要的是限制条件(也就是什么时候递归返回)
1.求二叉树节点个数BinaryTreeSize()
- 当节点为空节点开始返回,空节点不算一个节点,所以返回0
- 当节点不为空的时候,返回左右子树的节点数加当前节点数(也就是1)return BinaryTreeSize(root->left) + BinaryTreeSize(root->right) + 1
int BinaryTreeSize(BTNode* root)
{
if(root==NULL)
return 0;
else
return BinaryTreeSize(root->left) + BinaryTreeSize(root->right) + 1
}
上面的代码还可以简化:
int BinaryTreeSize(BTNode* root)
{
return root == NULL ? 0 : BinaryTreeSize(root->left) + BinaryTreeSize(root->right) + 1;
}
2 求二叉树叶子节点个数BinaryTreeLeafSize()
叶子节点就是左右子节点都是空节点
- 当节点是空节点返回0
- 当节点的左右子节点都是空节点返回1 if (root->left == NULL && root->right == NULL)//叶子节点就是左右子树为空 return 1;
- 以上两种情况都不属于向下找
int BinaryTreeLeafSize(BTNode* root)
{
if (root == NULL)
return 0;
if (root->left == NULL && root->right == NULL)//叶子节点就是左右子树为空
return 1;return BinaryTreeLeafSize(root->left) + BinaryTreeLeafSize(root->right);
}
3.求二叉树第k层节点个数 BinaryTreeLevelKSize()
当前节点的第k层=左子树的第k-1层+右子树的第k-1层
- 如果节点是空节点就返回0
- 如果k==1返回1
- 如果k!=1且节点不是空节点,就返回BinaryTreeLevelKSize(root->left, k - 1)+ BinaryTreeLevelKSize(root->right, k - 1)
int BinaryTreeLevelKSize(BTNode* root, int k)
{
if (root == NULL)
return 0;
if (k == 1)
return 1;return BinaryTreeLevelKSize(root->left, k - 1)+ BinaryTreeLevelKSize(root->right, k - 1);
}
4. 二叉树查找值为x的节点BinaryTreeFind()
这个题如果找到节点了,要注意返回值的接收
- 如果节点是空节点就返回NULL
- 如果节点的值就是x,返回该节点
- 如果该节点左子树函数调用的值不为0,则返回左子树函数调用的值
- 如果该节点右子树函数调用的值不为0,则返回右子树函数调用的值
- 以上情况都不符合说明没找到,返回NULL
//注意返回值的接收
BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{
if (root == NULL)
return NULL;if (root->val == x)
return root;BTNode* ret1 = BinaryTreeFind(root->left,x);
if (ret1)
return ret1;BTNode* ret2 = BinaryTreeFind(root->right,x);
if (ret2)
return ret2;return NULL;
}
二.堆
1.什么是堆?
堆是一颗完全二叉树,堆分为大堆和小堆,父节点总是大于子节点的树是大堆,父节点总是小于子节点的树是小堆。
堆是一个完全二叉树,所以可以用数组实现
2.向上调整建堆(建小堆)
比如:9,3,4,2,10,34,12,6要把这个数组建堆
向上调整建堆的关键:该元素的以上的元素都是堆,因为如果上面的元素都是堆,那么只要将该元素调整到正确位置,整个元素就是堆了
可以把数组第一个元素9看作堆的第一个元素,然后在堆底插入第二个元素3,此时3以上的元素都是堆,然后判断要不要交换顺序。依次插入4,2,10,34,12,6重复以上步骤
这里是向上调整建堆的代码:
- 创建一个结构体表示堆,结构体成员size表示元素个数,capacity表示堆的元素容量,Data存放元素
- AdjustUp()函数,通过子节点的下标kid找到父节点的下标int parent = (kid - 1) / 2,如果子节点比父节点小,那么交换节点且parent = (kid - 1) / 2,一直循环直到kid==0或子节点比父节点大
- HeapInit()函数,为堆Data开辟空间,让堆的元素个数和最大容量等于n,pc->size = n,pc->capacity = n,把数组a拷贝到Data中。然后把第一个元素看作堆,从第二个元素开始向上调整
typedef int HpDataType;
typedef struct Heap
{
int size;
int capacity;
int* Data;
}Heap;
//向上调整
//把下面的一个数向上提取
void AdjustUp(HpDataType* arr, int kid)//数组下标
{
assert(arr);
int parent = (kid - 1) / 2;
while (kid != 0)
{
if (arr[kid] < arr[parent])
{
Swap(&arr[kid], &arr[parent]);
kid = parent;
parent = (kid - 1) / 2;
}
else
{
break;
}
}
}
//交换函数
void Swap(HpDataType* pa, HpDataType* pb)
{
HpDataType tmp = *pa;
*pa = *pb;
*pb = tmp;
}
//堆的初始化
void HeapInit(Heap* pc, HpDataType* a, int n)
{
assert(pc);
assert(a);
Heap* tmp = (Heap*)malloc(sizeof(HpDataType) * n);
if (tmp == NULL)
{
perror("malloc error");
exit(-1);
}
pc->size = n;
pc->capacity = n;
pc->Data = tmp;
memcpy(pc->Data, a, (sizeof(HpDataType) * n));
//使用向上排序的前提是前面的数据是堆
//这里相当于把第一个看做是堆中的数据,从第二个数据开始插入堆中向上排序
int i = 0;
for (i = 1; i < n; i++)
{
AdjustUp(pc->Data, i);
}
}
3.向下调整建堆(建小堆)
比如:9,3,4,2,10,34,12,6要把这个数组建堆
向上调整建堆的关键:该元素的以下的元素都是堆,因为如果下面的元素都是堆,那么只要将该元素调整到正确位置,整个元素就是堆了
从第一个非叶子节点开始调整,非叶子节点以下节点可以看成是堆。然后从非叶子节点向下调整直到根节点
这是向下调整的代码:
- AdjustDown()函数,需要找出左右子节点中小的那一个kid, 先假设左孩子比较小kid = parent * 2 + 1,如果右孩子存在且比左孩子小,那么kid += 1,如果kid比parent小就交换,kid = parent * 2 + 1,一直重复直到kid<n或kid比parent大
- HeapInit()函数,最后一个节点的下标是n-1,第一个非叶子节点((n-1)-1)/2,一直向下调整直到根节点
//向下调整(需要找出小孩子)
//向下调整的前提是下面的数据是堆
//把上面的一个数向下放,也就是同时对比两个下面的数取其一
void AdjustDown(HpDataType* arr,int parent, int n)//找到第一个叶子节点需要最后一个节点的下标
{
int kid = parent * 2 + 1;
while (kid < n)
{
if (kid+1<n&&arr[kid + 1] < arr[kid])
{
kid += 1;
}
if (arr[parent] > arr[kid])
{
Swap(&arr[parent], &arr[kid]);
parent = kid;
kid = parent * 2 + 1;
}
else
{
break;
}
}
}
//交换函数
void Swap(HpDataType* pa, HpDataType* pb)
{
HpDataType tmp = *pa;
*pa = *pb;
*pb = tmp;
}
堆的初始化
void HeapInit(int* arr, int n)
{
for (int i = (n - 2) / 2; i >= 0; i--)//n-1是下标
{
AdjustDown(arr, i, n);
}
}
4.建堆的时间复杂度
向上调整建堆时间复杂度:O(Nlog2N)
向下调整建堆时间复杂度:O(N)
5.堆的实现
堆的插入删除和检查是否为空的操作和顺序表的细节大致一样,这里不再展开 顺序表的内容解析: 顺序表和链表
typedef int HpDataType;
typedef struct Heap
{
int size;
int capacity;
int* Data;
}Heap;
//堆的初始化
void HeapInit(Heap* pc, HpDataType* a, int n)
{
assert(pc);
assert(a);
Heap* tmp = (Heap*)malloc(sizeof(HpDataType) * n);
if (tmp == NULL)
{
perror("malloc error");
exit(-1);
}
pc->size = n;
pc->capacity = n;
pc->Data = tmp;
memcpy(pc->Data, a, (sizeof(HpDataType) * n));
//使用向上排序的前提是前面的数据是堆
//这里相当于把第一个看做是堆中的数据,从第二个数据开始插入堆中向上排序
int i = 0;
for (i = 1; i < n; i++)
{
AdjustUp(pc->Data, i);
}
}
//堆的销毁
void HeapDestory(Heap* pc)
{
assert(pc);
free(pc->Data);
pc->Data = NULL;
pc->size = pc->capacity = 0;
}
//堆的打印
void HeapPrint(Heap* pc)
{
int i = 0;
for (i = 0; i < pc->size; i++)
{
printf("%d ", pc->Data[i]);
}
printf("\n");
}
//向上调整
//把下面的一个数向上提取
void AdjustUp(HpDataType* arr, int kid)//数组下标
{
assert(arr);
int parent = (kid - 1) / 2;
while (kid != 0)
{
if (arr[kid] < arr[parent])
{
Swap(&arr[kid], &arr[parent]);
kid = parent;
parent = (kid - 1) / 2;
}
else
{
break;
}
}
}
//堆的插入
void HeapPush(Heap* pc, HpDataType x)
{
assert(pc);
if (pc->size == pc->capacity)
{
int NewCapacity = pc->capacity == 0 ? 4 : pc->capacity * 4;
Heap* tmp = (Heap*)realloc(pc->Data, sizeof(HpDataType) * NewCapacity);
if (tmp == NULL)
{
perror("realloc error");
exit(-1);
}
pc->Data = tmp;
}
pc->Data[pc->size] = x;
pc->size++;
AdjustUp(pc->Data, pc->size-1);
}
//交换函数
void Swap(HpDataType* pa, HpDataType* pb)
{
HpDataType tmp = *pa;
*pa = *pb;
*pb = tmp;
}
//向下调整(需要找出小孩子)
//向下调整的前提是下面的数据是堆
//把上面的一个数向下放,也就是同时对比两个下面的数取其一
void AdjustDown(HpDataType* arr,int parent, int n)//找到第一个叶子节点需要最后一个节点的下标
{
int kid = parent * 2 + 1;
while (kid < n)
{
if (kid+1<n&&arr[kid + 1] < arr[kid])
{
kid += 1;
}
if (arr[parent] > arr[kid])
{
Swap(&arr[parent], &arr[kid]);
parent = kid;
kid = parent * 2 + 1;
}
else
{
break;
}
}
}
//堆的删除
//此处让最后一个数据覆盖堆顶的数据,再向下排序
void HeapPop(Heap* pc)
{
assert(pc);
assert(pc->size > 0);
Swap(&pc->Data[0], &pc->Data[pc->size - 1]);
pc->size--;
AdjustDown(pc->Data,0,pc->size);
}
//判断堆是否为空
int HeapEmpty(Heap* pc)
{
assert(pc);
return pc->size == 0;
}
//堆的数据个数
int HeapSize(Heap* pc)
{
assert(pc);
return pc->size;
}
//获取栈顶数据
HpDataType HeapTop(Heap* pc)
{
assert(pc);
assert(!HeapEmpty(pc));
return pc->Data[0];
}