数据结构学习:二叉树

树的概念及遍历方式(铺垫)

树形结构:除根节点外其他节点仅有一个前驱结点(子树不相交

树的遍历方式左孩子右兄弟

先一直走左孩子,当前的左孩子没有孩子时走其右兄弟,走完后再走自己的右兄弟,即可保证走完

数组二叉树(最多只有两个子树的特殊树):

分类:

满二叉树:每层节点都到达了最大值(结点数: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;
	}

大概是这些,学习历时约两个星期,经验就是学了就要自己敲。

后面开始学排序

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值