树与二叉树, 堆与优先队列, 排序与查找

208 阅读3分钟

数据结构 = 结构定义 + 结构操作

树形结构

树的深度(从上至下)

树的高度(从下至上)

节点数量 = 边数+1

二叉树

  • 每个节点的度最多为2
  • 度为0的节点比度为2的节点多一个: n0=n2+1n_0 = n_2+1
  • 通过左孩子右兄弟的方法可以将N叉树转换为二叉树

二叉树 - 遍历

  • 前序遍历: 根 左 右
  • 中序遍历: 左 根 右
  • 后序遍历: 左 右 根

二叉树分类

国内教材分为完全二叉树和满二叉树

国外教材分为完全二叉树, 满二叉树(n1=0n_1=0)和完美二叉树(等同于国内的满二叉树)

image.png

二叉树 - 完全二叉树

image.png

完全二叉树展现的计算思维是: 记录式转计算式的思维, 是一种算法优化手段

二叉树 - 广义表

image.png

image.png

代码演示

// binary_tree_coding
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
// 结构定义 
typedef struct Node {
    int val;
    struct Node *lchild, *rchild; 
}Node;
typedef struct Tree {
	Node *root;
	int n; // 树存储的节点数 
}Tree;
// 初始化 
Node *getNewNode(int val){
	Node *p = (Node*)malloc(sizeof(Node));
	p->val = val;
	p->lchild = p->rchild = NULL;
	return p;
}
Tree *getNewTree() {
	Tree *tree = (Tree*)malloc(sizeof(Tree));
	tree->n = 0;
	tree->root = NULL;
	return tree;
}
// 销毁 
void clearNode(Node *node){
	if (node==NULL) return;
	clearNode(node->lchild);
	clearNode(node->rchild);
	free(node);
	return ;
}
void clearTree(Tree* tree){
	if (tree==NULL) return;
	clearNode(tree->root);
	free(tree);
	return ;
}
// 排序二叉树
Node *insertNode(Node *root, int val, int *ret){
	if (root==NULL) {
		*ret = 1;
		return getNewNode(val);
	}
	if (root->val==val) return root;
	if (root->val>val) root->lchild = insertNode(root->lchild, val, ret);
	else root->rchild = insertNode(root->rchild, val, ret);
	return root;
}
void insert(Tree *tree, int val){
	int flag = 0;
	tree->root = insertNode(tree->root, val, &flag);
	tree->n += flag;
	return ;
} 
// 二叉树转广义表
void outputNode(Node* root){
	if (root==NULL) return;
	printf("%d", root->val);
	if (root->lchild==NULL && root->rchild==NULL) return;
	printf("(");
	outputNode(root->lchild);
	printf(",");
	outputNode(root->rchild);
	printf(")");
	return ; 
}
void outputTree(Tree *tree){
	printf("tree(%d) = ", tree->n);
	outputNode(tree->root);
	printf("\n");
	return ;
}
// 前序遍历
void preorderNode(Node *node) {
	if (node==NULL) return;
	printf("%d ", node->val);
	preorderNode(node->lchild);
	preorderNode(node->rchild);
	return ;
}
void preorder(Tree *tree) {
	printf("preorder : ");
	preorderNode(tree->root);
	printf("\n");
}
// 中序遍历
void inorderNode(Node *node) {
	if (node==NULL) return;
	inorderNode(node->lchild);
	printf("%d ", node->val);
	inorderNode(node->rchild);
	return ;
}
void inorder(Tree *tree) {
	printf("inorder : ");
	inorderNode(tree->root);
	printf("\n");
}
// 后序遍历
void postorderNode(Node *node) {
	if (node==NULL) return;
	postorderNode(node->lchild);
	postorderNode(node->rchild);
	printf("%d ", node->val);
	return ;
}
void postorder(Tree *tree) {
	printf("postorder : ");
	postorderNode(tree->root);
	printf("\n");
}
// 主函数 
int main() {
	srand(time(0));
	Tree *tree = getNewTree();
	for (int i=0; i<10; i++) {
		int val = rand()%100;
		insert(tree, val);
		outputTree(tree);
	}
	preorder(tree);
	inorder(tree);
	postorder(tree);
	clearTree(tree);
	system("pause");
	return 0;
} 

堆与优先队列

堆本质上是一个完全二叉树, 一个堆就可以对应一个连续的数组

  • 大顶堆
  • 小顶堆

堆 - 尾部插入调整

堆在进行插入操作时, 时间复杂度为lognlog_n次(n是堆内部的元素数量)

image.png

堆 - 头部弹出调整

image.png

image.png

image.png

堆排序

时间复杂度为nlognnlog_n (n是堆内部的元素数量)

口诀:

  • 将堆顶元素与堆尾元素交换
  • 将此操作看作是堆顶元素弹出操作
  • 按照头部弹出以后的策略调整堆

image.png

image.png

堆与优先队列

优先队列的底层就是堆, 都是从尾部进入, 从头部出去, 优先队列中优先的含义是每次出队的都是全局最值

普通队列最大(最小)堆
尾部出队尾部可以插入
头部出队头部可以弹出
先进先出每次出队权值(最值元素)
数组实现数组实现, 逻辑上看成一个堆

题目

LEETCODE 703 23 295 264 313

代码实现

// heap_sort
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define swap(a, b) { \
	__typeof(a) __temp = a; \
	a = b, b = __temp; \
}
 
void downUpdate(int *arr, int n, int ind){
    while ((ind<<1)<=n) {
		int temp = ind, l=ind<<1, r=ind<<1|1;
		// 从小到大排序建立大顶堆 
		if (arr[l]>arr[temp]) temp=l;
		if (r<=n && arr[r]>arr[temp]) temp=r;
		if (temp==ind) break;
		swap(arr[temp], arr[ind]);
		ind = temp; 
    }
    return;
}

void heap_sort(int *arr, int n) {
	// 改变arr的下标, 从0开头变为从1开头 
	arr -= 1;
	for (int i=n>>1; i>=1; --i) {
		downUpdate(arr, n, i);
	}
	for (int i=n; i>1; --i){
		swap(arr[1], arr[i]);
		downUpdate(arr, i-1, 1);
	}
	return ; 
}

void output(int *arr, int n) {
	printf("arr[%d] = [", n);
	for (int i=0; i<n; i++){
		printf("%d ", arr[i]);
	}
	printf("]\n");
	return ;
}

int main(){
	srand(time(0));
	#define MAX_OP 20
	int *arr = (int*)malloc(sizeof(int)*MAX_OP);
	for (int i=0; i<MAX_OP; i++){
		int val = rand()%100;
		arr[i] = val;
	}
	output(arr, MAX_OP);
	
	heap_sort(arr, MAX_OP);
	output(arr, MAX_OP);
	return 0;
}

排序与查找

稳定排序(插入, 冒泡, 归并)

a[i]a[i]=a[j]a[j](i<j), 但是排序之后二者的相对顺序不变, 也就是说a[i]a[i]仍然在a[j]a[j]前面, 则称为稳定排序算法

  • 插入 n2n^2
  • 冒泡 n2n^2
  • 归并 nlognn*log_n

插入排序

口诀:

  • 将数组分成"已排序区域"和"未排序区域"
  • 将"未排序区域"的头部元素, 插入到"已排序区域"中
  • 直到"未排序区域"没有元素为止

平均时间复杂度: O(n2n^2)

02+12+...+n12=n(n1)4\frac{0}{2} + \frac{1}{2} + ... + \frac{n-1}{2} = \frac{n(n-1)}{4}