树的概念及遍历方式(铺垫)
树形结构:除根节点外其他节点仅有一个前驱结点(子树不相交)
树的遍历方式:左孩子右兄弟
先一直走左孩子,当前的左孩子没有孩子时走其右兄弟,走完后再走自己的右兄弟,即可保证走完
数组二叉树(最多只有两个子树的特殊树):
分类:
满二叉树:每层节点都到达了最大值(结点数:2^(n-1)-1)
完全二叉树:前n-1层满,最底一层从左到右是连续的
规律:二叉树度为0的节点总比度为2的节点多一个(推导:当产生一个度为2的节点,必然产生度为0的节点,互相抵消;当产生度为1的节点,0/2都不会增加;而最初为0的数量为1,度为2的结点数为0)
堆(数据按二叉树按顺序存储:
分为大根堆/小根堆:自顶向下从大到小/从小到大
性质:堆中某个结点的值总是不大于或不小于其父结点的值;堆总是一棵完全二叉树
代码实现
二叉树有数组形态和链表形态,先讲讲数组形态:
下标关系:leftchiled=parent*2+1;
rightchiled=parent*2+2;
parent=(child-1)/2(不论左右,自动取整);
(附数组形态的局限性:即使当前节点没有孩子,也要空出两个空,保证可以找到对应的下标。因此适用于完全二叉树的情况)
堆的实现
创建:先使用一个结构体包好,类似顺序表,包含数组指针,当前大小(快速找尾),容量
#pragma once
#include <stdio.h>
#include<stdlib.h>
#include <string.h>
#include <assert.h>
#include <malloc.h>
#include<stdbool.h>
typedef int datatype;
typedef struct heap {
datatype* data;
int size;
int capacity;
}Heap;//
void Heapinit(Heap*hp);
void Heapdestroy(Heap*hp);//销毁结构体,那里面的指针会一起销毁吗?free只适合malloc出来的东西,如果结构体占用的是分配的内存,是不会销毁的,必须由内到外销毁;在这里用置为NULL处理。
void swap(int* p1, int* p2);
void Heappushback(Heap* hp, datatype data);
void Heappopback(Heap* hp);
void Heaptop(Heap* hp);
void Heapsize(Heap*hp);
bool Heapempty(Heap* hp);
void Adjustup(datatype* data, int parent, int child);
void Adjustdown(datatype* data, int parent, int size);
#include"head.h"
void Heapinit(Heap* hp) {
assert(hp);
hp->data = (datatype)malloc(sizeof(datatype) * 4);
hp->capacity = 4;
hp->size = 0;
}
void swap(int* p1,int *p2) {
int temp = *p1;
*p1 = *p2;
*p2 = temp;
}
void Adjustup(datatype*data,int parent,int child) {//边界
while (child != 0) {
if (data[child] > data[parent]) {
swap(&data[child], &data[parent]);
child = parent;
parent = (parent - 1) / 2;
}
else {
break;
}
}
}
void Adjustdown(datatype* data,int parent,int size) {
assert(data);
int leftchild = parent * 2 + 1;
while (parent<=size&&leftchild<=size) {
if (leftchild+1 <= size) {
if (data[leftchild] < data[leftchild + 1]) {
leftchild = leftchild + 1;
}
}
if (data[leftchild]>data[parent]){
swap(&data[leftchild], &data[parent]);
parent = leftchild;
leftchild = leftchild * 2 + 1;
}
else {
break;
}
}
}
void Heappushback(Heap* hp, datatype data) {
assert(hp);
assert(hp->data);
if (Heapempty(hp)) {
hp->data[0] = data;
hp->size++;
return;
}
if (hp->size == hp->capacity) {
int *temp = (datatype*)realloc(hp->data, sizeof(datatype)*hp->capacity * 2);//单位是字节
hp->data = temp;
hp->capacity *= 2;
}
hp->data[hp->size] = data;
Adjustup(hp->data,(hp->size-1)/2, hp->size);
hp->size++;
}
void Heappopback(Heap* hp) {
assert(hp);
swap(&hp->data[0], &hp->data[hp->size - 1]);
hp->size--;
Adjustdown(hp->data, 0,hp->size-1);
}
void Heapsize(Heap* hp) {
assert(hp);
printf("%d", sizeof(hp->size));
}
void Heaptop(Heap* hp) {
assert(hp);
printf("%d", hp->data[0]);
return;
}
bool Heapempty(Heap* hp) {
if (hp->size == 0) {
return true;
}
else {
return false;
}
}
使用堆实现排序的步骤:
建堆可以向上调整/向下调整(复杂度有所不同)
向上调整建堆相当于模拟插入建堆,数组一个个往下走,依次向上调整(n*logn)
向下调整,从最后一个子树开始往前向下调整(一层层调好),保证每次向下调整时左右子树都是堆(向上和向下都是要满足这个条件的,刚刚向上调整也是相当于一层层调成了堆)(n,直觉上市n*logn,用错位相减法证明,直接想的话,因为向下调整忽略最后一层,向上调整忽略第一层,明显数量上差距就很大;此外观察每一层,结点数少的层向上调整需要的次数反而多,向下调整的次数反而少)
两种方式都不能保证有序,只是保证了堆结构,所以接下来要进行排序
自己的疑问:建堆是为什么要让左右孩子大的那个上去呢?而不是直接比较左孩子和父结点呢?
30 40 50这种就不能满足,除非进行多次向下调整,但是很少这种情况,另外在二叉树结构里面,删除操作只会进行一次。
堆排序的实现(向下调整):
根节点一定是最大的,因此让根节点和末尾节点交换,交换后的根节点向下调整,再次将第二大的数置于根节点,同时末尾节点必然不会动(第一大)。下次把调整的范围缩小一个单位,以此类推。(因此排升序建大堆,排降序减小堆)这里的时间复杂度(n*logn)
疑问:末尾节点不一定是最小的数,这样子会不会有什么影响呢?为什么必然实现排序呢?
根本不用去想左右会不会不符合排序方式,因为每次操作就是把最大的数放到了当前范围的最后,宏观地看待这个过程,最后必然实现排序。我在这里困扰了很久。
在main函数里面实现就好了
#include "head.h"
int main() {
int data[10] = { 4,7,5,3,6,9,1,12,17,55 };
//建堆,向下调整
int size = sizeof(data) / sizeof(data[0]);
for (int i =( size - 1 - 1) / 2; i >= 0; i--){
Adjustdown(data, i, size-1);
}
//堆排序
int i = size - 1;
while(i>=0){
swap(&data[i],&data[0]);
i--;//一交换完成就缩小范围
Adjustdown(data, 0, i);
}
return 0;
}
TOPK问题
在给定的N个数据中选出最大前K个,用堆的方法实现的话
用前K个数据建一个小堆,遍历后N-K个,如果比堆顶大就替换堆顶并向下调整。(logk*N)
为什么不建大堆替换堆顶向下调或替换最后一个树向上调整?
如果建大堆替换堆顶,若一开始在堆顶的就是最大的数,别的数就无法进入堆。第二种情况是因为这样可能导致左子树没有洗牌洗干净(相当于模拟建堆,没有排序作用)
链式二叉树学习
typedef struct TreeNode {
int val;
struct TreeNode* left;
struct TreeNode* right;
}TreeNode;
掌握先中后序遍历
要能清楚认知到这几种顺序的逻辑,因为之前参加蓝桥杯已经学过dfs了,所以理解起来比较容易不详述
一些小问题:
如何获得当前层的节点个数?如何获得树的高度?如何获得节点个数?
(涉及函数参数和返回值的设计以及对递归的理解)
层序遍历的实现:使用队列实现
先让一个节点进入队列,再使其左右孩子进入队列(连贯操作)
//层序遍历2
void Levellorder(TreeNode*root) {
Queue q;
Queueinit(&q);
if (Queueempty(&q)) {
QueuePush(&q,root);
}
while (!Queueempty(&q)) {
TreeNode* front = Queuefront(&q);
QueuePop(&q);
printf("%d ", front->val);
if (front->left)
QueuePush(&q, front->left);
if (front->right) {
QueuePush(&q, front->right);
}
}
QueueDestroy(&q);
return;
}
判断完全二叉树
原理:所有的非空节点都是连续的,使用层序遍历,但空节点也放入队列中,若第一次出现空后又出现了了非空则不是完全二叉树。
bool Checkfulitree(TreeNode*root) {
Queue q;
Queueinit(&q);
if (Queueempty(&q)) {
QueuePush(&q, root);
}
int cnt=0;
while (!Queueempty(&q)) {
TreeNode* front = Queuefront(&q);
QueuePop(&q);
if (cnt > 0 && front) {
return false;
}
if (!front) {
cnt++;
continue;
}
QueuePush(&q, front->left);
QueuePush(&q, front->right);
}
QueueDestroy(&q);
return true;
}
大概是这些,学习历时约两个星期,经验就是学了就要自己敲。
后面开始学排序