数据结构编程实验——chapter10-应用经典二叉树编程
二叉树不仅结构简单、节省内存,更重要是是这种结构有利于对数据的二分处理。之前我们提过,在二叉树的基础上能够派生很多经典的数据结构,也是下面我们将进行讨论的知识点:
(1) 提高数据查找效率的二叉排序树。
(2) 优先队列的最佳存储结构的二叉堆。
(3) 兼具二叉排序树和二叉堆性质的树堆。
(4) 用于算法分析的数据编码的哈夫曼树。
一. 二叉排序树
二叉排序树主要用于高效率查找。查找方法一般有三种:顺序查找、二分查找和二叉排序树查找。二叉排序树又可以分成多种类型。这里不同的查找方法,衡量他们的关键就是查找效率,往往越是复杂的构造二叉排序树,在查找数据的效率方面越优良。
具有以下性质的非空二叉树,称为二叉排序树:
1) 若根节点的左子树不空,则左子树的所有节点值均小于根节点值。
2) 若根节点的右子树不空,则右子树的所有节点值均不小于根节点值。
3) 根节点的左右子树也分别是二叉排序树。
容易看到,根据二叉排序树的构造机制,查找某个元素的效率就取决于所构造的二叉排序树的深度。深度越小,效率越高。二叉排序树有如下三种类型:
i) 普通二叉排序树:边输入边构造的二叉排序树,树的深度取决于输入的序列。
ii) 静态二叉排序树:按照二分查找的方法构造出的二叉排序树,近似丰满,深度约为logn。但是这种树需要离线构建,即输入数据后一次性建树,不方便动态维护。
iii) 平衡树:再插入和删除过程中一直保持左右子树的高度至多相差1的平衡条件,且能够保证树的深度是logn.
Ex1(poj2309):
给出一种无穷满二叉排序树的机制,其叶子节点是无穷的奇数序列:1、3、5……然后1、3的父节点是右儿子序号减左儿子序号.这样能够形成倒数第二层的节点,然后按照同样的机制形成上面那层节点。然后给出一个节点序号x,编程计算以该节点为根的二叉排序树的最小编号以及最大编号。
既然题目给出的是无穷满二叉排序树,我们就能够充分利用二叉排序树和满二叉树的性质。对于以x为根的子树,如果我们知道这棵树的层数k(层数从0开始计数),根据满二叉树的性质,该子树有2^(k+1) – 1个节点。x的左子树的节点编号都是小于x的,x的左子树也是一个满二叉树,节点数是2^k – 1,因此我们能看到,x的左子树的编号区间是[min , x - 1],这个区间含有的整数就是x的左子树的节点数,即有x-1-min+1=2^k – 1成立。即有min = x – 2^k +1.对称的,对于最大标号的节点,是完全一样的分析思路。则有max = x + 2^k – 1.
下面我们需要解决的问题就剩下如何计算根节点为x的树的层数。根据这棵无限二叉排序树的机制,我们容易看到规律,x除以2^k是奇数的时候,k便是层数。我们将整数x视为二进制形式a[n]a[n-1]…a[0],再从按权展开的形式去考察这个整数x,我们反复除以2,发现当遇到二进制形式右侧第一个1的时候,整数x变成了奇数。即我们有这样的结论,二进制数x右侧第一个1所在位置的权值2^k,k便是树的层数。
而这样的2^k我们可以通过位运算x&(-x)快速得到。
参考代码如下:
#include<iostream> using namespace std; long long lowbit(long long x){ return x & (-x); } int main(){ long long n , x; cin >> n; for(int i = 0;i < n;i++){ cin >> x; cout << x - lowbit(x) + 1 <<' ' << x + lowbit(x) - 1<<endl; } }
二.二叉堆.
二叉堆是一棵满足下列性质的完全二叉树:如果某节点有孩子,则根节点的值都小于孩子节点的值,这样的二叉堆我们称之位小根堆。反之,如果根节点的值都大于孩子节点的值,我们称之为大根堆。
基于二叉堆的性质,我们非常好获得整个堆的最大元素和最小元素,因此二叉堆经常用于优先队列的存储结构。因为优先队列的删除操作正是删除一个线性序列中优先级最大或者最小的元素。
下面我们便开始讨论这样一个数据结构的各种操作。
1) 小根堆的插入操作:
假设我们原本有一个满足二叉堆性质的结构(在程序中我们用线性结构存储这样一个特殊的完全完全二叉树),当需要加入一个新的节点进入小根堆的时候,我们将其加入到小根堆的最后面,然后根据其节点序号(注意和节点权值不一样)来得到其父节点的序号,进而访问其权值,进行比较并进行交换,然后进行迭代操作一直向上走。
Int k = ++top; Heap[k] = 被插入元素的权值. While(k > 0){//k=0到达根节点,是迭代的终结点. int t = (k - 1)/2; if(heap(k )< heap(t)){ swap(heap[k] , heap[t]) k = t; } else break; }
2) 小根堆的删除操作
在堆中删除最小元素,即删除heap[0],然后将二叉堆中节点序号最大的节点移动到根节点,然后从根节点往下进行维护小根堆性质的操作。
if(top){ int temp = heap[0]; int k = 0; heap[k] = heap[top--]; while((2 * k + 1) <= top){ int t = 2*k + 1; if(t < top && heap[t + 1] < heap[t]) t++; if(heap[k] > heap[t]){//当前节点的最小孩子比当前节点的权值小,需要交换 swap(heap[k] , heap[t]); k = t; } else break; } } else output "empty heap"
ex1(zoj2724):
消息队列是操作系统的基础,现在给出两种类型的指令:
1)GET指令:获取消息队列中优先级最高的指令信息(包括指令名称和指令参数)。
2)PUT指令:往消息队列中添加指令,包括指令名称,指令参数和优先值。注意这里优先值越小优先级越大,二者相同时,较早进入消息队列的优先级高。
典型的优先队列的题目,这里我们在用二叉堆实现的时候,需要手写一个函数用于二叉堆元素权值的比较。输入的时候我们为每个节点顺序标号,节点信息我们存储在一个缓存区,而二叉堆用于存储节点序号。在插入删除的时候,我们利用存储的节点序号这一索引,在缓冲区找到对应节点的信息,进行比较。
参考代码如下:
#include<cstdio> #include<cstring> #include<algorithm> using namespace std; const int maxn = 60000 + 10;//60000条指令 const int maxs = 100; struct info{ char name[maxs]; int para; int pri , t; }p[maxn]; int heap[maxn]; int top , used; int compare(int a , int b){//ad额优先级高发返回-1 ,否则返回1 if(p[a].pri < p[b].pri) return -1; if(p[a].pri > p[b].pri) return 1; if(p[a].t < p[b].t) return -1; //优先级相等,看时间戳 if(p[a].t > p[b].t) return 1; } int main(){ used = 0; top = 0;//堆尾指针,指向堆数组最后一个元素的后面。 int cnt = 0; char s[maxs]; while(scanf("%s" , s) != EOF){ getchar(); if(!strcmp(s , "GET")){//输入GET,进行删除操作 if(top){ printf("%s %d\n" , p[heap[0]].name , p[heap[0]].para); int k = 0; heap[k] = heap[--top]; while(2 * k + 1 < top){//这里注意取值范围,top是指堆的尾部指针的后面那个一个,因此这里就不取等号 int t = 2 * k + 1; if(t < top && compare(heap[t + 1] , heap[t] < 0)) t++; if(t < top && compare(heap[t] , heap[k]) < 0){ swap(heap[t] , heap[k]); k = t; } } } else printf("EMPTY QUEUE!\n"); } else{ scanf("%s %d %d" , p[cnt].name , &p[cnt].para , &p[cnt].pri); p[cnt].t = cnt; int k = top++; heap[k] = cnt++;//堆尾放入当前节点序号 while(k > 0){//第一个点不走这个 int t = (k - 1)/2; if(compare(heap[k] , heap[t]) < 0){ swap(heap[k] , heap[t]); k = t; } else break; } } } return 0; }