算法导论笔记:10基本数据结构(番外)
2015-04-25 16:27
423 查看
一:基本数据结构之栈队列链表树
1:数据结构就是一种动态的可变集合,不同的算法对动态集合有不同的操作,支持插入,删除,测试元素是否属于集合这些操作的动态集合成为字典。集合中的对象,一般都有关键字,有的对象还有卫星数据。
动态集合上的操作可以分为:查询操作和修改操作,任何具体应用通常只会进行下面操作的若干个:搜索,插入,删除,最小值,最大值,前继,后继。
2:栈、队列、链表
栈是先进后出,队列是先进先出。二者既可以用数组实现,也可以用链表实现。
链表可以有很多形式,可以是单向的或者双向的,可以是有序的或者无序的,也可以是循环的。
3:无指针情况下的实现。
有些语言不支持指针,需要其他的方法来实现链式数据结构,比如数组:
下图说明了如何用三个数组表示链表,三个数组分别代表了key,next,prev。三个数组项key[x],next[x],prev[x]一起表示链表中的一个对象。
也可以使用一维数组来实现,数组中每三个元素表示链表中的一个对象,比如下图:
4:有根树
链式数据结构除了表示链表之外,还可以表示有根树,比如二叉树,每个结点包含父指针,左孩子,右孩子,如下图:
二叉树的表示方法可以推广到每个结点的孩子数最多为常数k的任意类型的树:只需要将left,right用child1,child2,…,childk代替即可。但是当孩子的节点数无限制的时候,这种方法就失效了。
可以使用左孩子右兄弟表示法,此种方法只需要O(n)的存储空间。这种方法中,每个结点中包含,父指针p,x.leftchild指向结点x最左边的孩子,x.rightsibling指向x右侧相邻的兄弟结点。如下图:
二;栈
1:栈的顺序表示:
一般来说,在初始化空栈时不应限定栈的最大容量,较合理的做法是:先为栈分配一个基本容量,然后在程序中,当栈的空间不够使用时在逐渐扩大。base表示栈底,top表示栈顶。当top=base时,表示栈空。每当插入新的栈顶元素时,top增加1,删除栈顶元素时,top减少1.所以,非空栈中的top指针始终在栈顶元素的下一个位置上。如下图:
栈的实现:
void InitStack(Stack *s)
{
s->base = (SElemType *)malloc(STACK_INIT_SIZE * sizeof(SElemType));
if(s->base == NULL)
{
printf("Init Stack mallocerror\n");
return;
}
s->top = s->base;
s->stacksize = STACK_INIT_SIZE;
}
int isStackEmpty(Stack *s)
{
if(s->top ==s->base)
{
//printf("the stack isempty\n");
return true;
}
return false;
}
int stacklen(Stack *s)
{
return s->top - s->base;
}
void GetTop(Stack*s, SElemType *e)
{
if(s->top == s->base)
{
printf("the stack isempty\n");
return;
}
*e = *(s->top - 1);
}
void push(Stack *s, SElemType e)
{
if(s->top - s->base >=s->stacksize)
{
s->base = (SElemType*)realloc(s->base,
(s->stacksize+ STACKINCREMENT) * sizeof(SElemType));
if(s->base == NULL)
{
printf("increase Stackerror\n");
return;
}
s->top = s->base + s->stacksize;
s->stacksize += STACKINCREMENT;
}
*(s->top ++) = e;
}
void pop(Stack *s, SElemType *e)
{
if(s->top == s->base)
{
printf("the stack isempty\n");
return;
}
*e = *(--(s->top));
}
2:迷宫求解
求迷宫中从入口到出口的所有路径是一个经典的程序设计问题。由于计算机解迷宫时,通常用“穷举求解”的方法,即从入口出发,顺某一方向向前搜索,若能走通,则继续向前走,否则原路退回,换一个方向在继续探索,直到所有的通路都探索到为止。为了保证在任何位置上都能够沿原路退回,需要栈来保存当前路径。
可以用二维数组表示迷宫,其中1表示墙,0表示通路:
maze mz = { 1,1,1,1,1,1,1,1,1,1,
1,0,0,1,0,0,0,1,0,1,
1,0,0,1,0,0,0,1,0,1,
1,0,0,0,0,1,1,0,0,1,
1,0,1,1,1,0,0,0,0,1,
1,0,0,0,1,0,0,0,0,1,
1,0,1,0,0,0,1,0,0,1,
1,0,1,1,1,0,1,1,0,1,
1,1,0,0,0,0,0,0,0,1,
1,1,1,1,1,1,1,1,1,1};
代码参见《迷宫问题》。
3:表达式求值
表达式求值是栈应用的又一个典型例子。这里介绍一种简单直观、广为使用的算法,通常称为“算符优先法”。
要对下面的算术表达式求值:4 + 2 × 3 - 10/5
首先要了解算术四则运算的规则。即:
(1)先乘除,后加减;
(2)从左算到右;
(3)先括号内,后括号外。
由此,这个算术表达式的计算顺序应为
4 + 2 × 3 - 10/5 = 4 + 6 - 10/5 = 10 - 10/5 = 10 - 2 =8
算符优先法就是根据这个运算优先关系的规定来实现对表达式的编译或解释执行的。
任何一个表达式都是由操作数(operand)、运算符(operator)和界限符(delimiter)组成的,为了叙述的简洁,我们仅讨论简单算术表达式的求值问题。这种表达式只含加、减、乘、除4种运算符。
我们把运算符和界限符统称为算符,它们构成的集合命名为OP。根据上述3条运算规则,在运算的每一步中,任意两个相继出现的算符θ1和θ2之间的优先关系至多是下面3种关系之一:
θ1<θ2 θ1的优先权低于θ2
θ1=θ2 θ1的优先权等于θ2
θ1>θ2 θ1的优先权高于θ2
表1定义了算符之间的这种优先关系:
由规则(3),+、-、*和/为θ1时的优先性均低于“(”但高于“)”,由规则(2),当θ1=θ2时,令θ1>θ2,“#”是表达式的结束符。为了算法简洁,在表达式的最左边也虚设一个“#”构成整个表达式的一对括号。表中的“(”=“)”表示当左右括号相遇时,括号内的运算已经完成。同理,“#”=“#”表示整个表达式求值完毕。“)”与“(”、“#”与“)”以及“(”与“#”之间无优先关系,这是因为表达式中不允许它们相继出现,一旦遇到这种情况,则可以认为出现了语法错误。在下面的讨论中,我们暂假定所输入的表达式不会出现语法错误。
为实现算符优先算法,可以使用两个工作栈。一个称做OPTR,用以寄存运算符;另一个称做OPND,用以寄存操作数或运算结果。算法的基本思想的:
(1)首先置操作数栈为空栈,表达式起始符“#”为运算符栈的栈底元素;
(2)依次读入表达式中每个字符,若是操作数则进 OPND栈,若是运算符则和OPTR栈的栈顶运算符比较优先权后作相应操作,直至整个表达式求值完毕(即
OPTR栈的栈顶元素和当前读入的字符均为“#”)。
以下算法描述了这个求值过程。
OperandType EvaluateExpression()
{
// 算术表达式求值的算符优先算法。设 OPTR 和 OPND 分别为运算符栈和运算数栈,OP 为运算符集合。
InitStack(OPTR);Push(OPTR,'#');
InitStack(OPND);c = getchar();
while(c!='#' || GetTop(OPTR)!='#')
{
if(!In(c,OP))
{ Push((OPND,c); c = getchar(); } // 不是运算符则进栈
else
switch(Precede(GetTop(OPTR),c))
{
case '<': // 栈顶元素优先权低
Push(OPTR,c); c = getchar();
break;
case '=': // 脱括号并接收下一字符
Pop(OPTR,x); c = getchar();
break;
case'>': // 退栈并将运算结果入栈
Pop(OPTR,theta);
Pop(OPND,b); Pop(OPND,a);
Push(OPND,Operate(a,theta,b));
break;
}// switch
}// while
return GetTop(OPND);
}// EvaluateExpression
3:汉诺塔问题:
void hanoi(intn,char x, char y, char z)
{
//将塔座x上按直径有小到大且自上而下编号为1至n的n个圆盘按规则搬动到
//塔座z上,y塔座可以用做辅助
if(n==1)
{
move(x,1,z);//当n==1时,则搬动x上的圆盘放到z上
}
else
{
hanoi(n-1,x,z,y);//将x上编号为1至n-1的圆盘移动到y,z做辅助塔
move(x,n,z);//执行搬动操作
hanoi(n-1,y,x,z);//将y塔座上的编号为1至n-1的圆盘移动到z上,x做辅助塔
}
}
三:队列
允许插入的一端叫对尾,允许删除的一端叫队头。
双端队列,限定插入和删除操作在表的两端进行的线性表,如下图:
和顺序栈相类似,在队列的顺序存储结构中,除了用一组地址连续的存储单元依次存放从队列头到队列尾的元素之外,尚需附设两个指针front和rear分别指向队列头元素和队列尾元素的位置。初始化建空队列时,令front=rear=0,每当插入新的队列尾元素时,“尾指针增1”;每当删除队列头元素时,“头指针增1”。因此,在非空队列中,头指针始终指向队列头元素,而尾指针始终指向队列尾元素的下一个位置。如图4所示。
图4 头、尾指针和队列中元素之间的关系
(a)空队列;(b)J1、J2和J3相继入队列;(c)J1和J2相继被删除;(d)J4、J5和J6相继插入队列之后J3及J4被删除
一个较巧妙的办法是将顺序队列臆造为一个环状的空间,如图5所示,称之为循环队列。指针和队列元素之间关系不变,如图6(a)所示循环队列中,队列头元素时J3,队列尾元素是J5,之后J6、J7和J8相继插入,则队列空间均被占满,如图6(b)所示,此时Q.front=Q.rear;反之,若J3、J4和J5相继从图6(a)的队列中删除,使队列呈“空”的状态,如图6(c)所示。此时亦存在关系式Q.front=Q.rear,由此可见,只凭等式Q.front=Q.rear无法判别队列空间是“空”还是“满”。可由两种处理方法:其一是另设一个标志位以区别队列是“空”还是“满”;其二是少用一个元素空间,约定以“队列头指针在队列尾指针的下一位置(指环状的下一位置)上”作为队列呈“满”状态的标志。
图5 循环队列示意图
图6 循环队列的头尾指针 (a)一般情况;(b)队列满时;(c)空队列;
1:循环队列实现(数组,空一个元素):
void InitQueue(SqQueue &Q)
{
Q.base = (QElemType *)malloc(MAXQSIZE * sizeof(QElemType));
if(!Q.base) exit (-1);
Q.front=Q.rear=0;
return;
}
int QueueLength(SqQueue Q)
{
return (Q.rear -Q.front + MAXQSIZE) % MAXQSIZE;
}
void EnQueue(SqQueue &Q, QElemType e)
{
if((Q.rear+1) %MAXQSIZE == Q.front)
{
printf("queue isfull\n");
return;
}
Q.base[Q.rear] = e;
Q.rear = (Q.rear+1) % MAXQSIZE;
return;
}
void DeQueue(SqQueue &Q, QElemType &e)
{
if(Q.front ==Q.rear)
{
printf("queue isempty\n");
return;
}
e = Q.base[Q.front];
Q.front = (Q.front+1) % MAXQSIZE;
return;
}
2:循环队列实现(标记法)
#define QUEUESIZE 10
//定义循环队列结构体
typedef struct _CycleQueue
{
int front;//队首
int rear;//队尾
int count;//队列中元素个数
int data[QUEUESIZE];//存储队列元素
} CycleQueue;
void InitQueue(CycleQueue *queue)
{
queue->front=0;
queue->rear=0;
queue->count=0;
}
int InQueue(CycleQueue *queue,intdata)
{
if (IsQueueFull(queue))
{
return 0;
}
else
{
queue->data[queue->rear]=data;
queue->count++;
queue->rear=(queue->rear+1)%QUEUESIZE;
return 1;
}
}
int OutQueue(CycleQueue *queue,int*pdata)
{
if (IsQueueEmpty(queue))
{
return 0;
}
else
{
*pdata=queue->data[queue->front];
queue->count--;
queue->front=(queue->front+1)%QUEUESIZE;
return 1;
}
}
int IsQueueEmpty(CycleQueue *queue)
{
return queue->count==0;
}
int IsQueueFull(CycleQueue *queue)
{
return queue->count==QUEUESIZE;
}
3:循环队列(多线程同步)
#define std_size 2000
#include<pthread.h>
typedef unsigned int uint;
template <classT>
class SyncFifo
{
protected:
uint in, out;
uint size;
T **tab;
pthread_mutex_t lock;
pthread_cond_t nonEmpty;
public:
/* Specific constructor */
SyncFifo (uint size = std_size);
/* Destructor */
~SyncFifo ();
/* get the first object */
T *get ();
/* get the first object (non totallyblocking)
* return NULL if there is none
*/
T *tryGet ();
/* add an object in the Fifo */
void put (T *obj);
/* how many itmes are there inside ? */
int getLength ();
bool isNonEmpty ();
void waitnoempty()
{
pthread_mutex_lock(&lock);
while(in == out)
{
pthread_cond_wait(&nonEmpty,&lock);
}
pthread_mutex_unlock(&lock);
}
};
template <classT>
SyncFifo<T>::SyncFifo(uint size)
{
tab = new T*[size];
this->size = size;
in = 0;
out = 0;
pthread_mutex_init (&lock, NULL);
pthread_cond_init (&nonEmpty, NULL);
}
template <classT>
SyncFifo<T>::~SyncFifo()
{
delete [] tab;
pthread_mutex_destroy (&lock);
pthread_cond_destroy (&nonEmpty);
}
template <classT>
T * SyncFifo<T>::get ()
{
T *tmp;
pthread_mutex_lock(&lock);
while(in == out)
{
pthread_cond_wait(&nonEmpty,&lock);
}
tmp = tab[out];
out = (out + 1) % size;
pthread_mutex_unlock(&lock);
return tmp;
}
template <classT>
T *SyncFifo<T>::tryGet ()
{
T *tmp = NULL;
pthread_mutex_lock(&lock);
if (in != out)
{
// The stack is not empty
tmp = tab[out];
out = (out + 1) % size;
}
pthread_mutex_unlock(&lock);
return tmp;
}
template <classT>
void SyncFifo<T>::put (T *obj)
{
pthread_mutex_lock(&lock);
tab[in] = obj;
if (in == out)
{
pthread_cond_broadcast(&nonEmpty);
}
in = (in + 1) % size;
if (in == out)
{
T **tmp;
tmp = new T*[2*size];
for (uint i=out; i<size; i++)
{
tmp[i]= tab[i];
}
for (uint i=0; i<in; i++)
{
tmp[i+size] = tab[i];
}
in += size;
size *= 2;
delete [] tab;
tab = tmp;
}
pthread_mutex_unlock(&lock);
}
template <classT>
int SyncFifo<T>::getLength ()
{
int tmp;
pthread_mutex_lock(&lock);
tmp = (in + size - out) % size;
pthread_mutex_unlock(&lock);
return tmp;
}
template <classT>
bool SyncFifo<T>::isNonEmpty ()
{
pthread_mutex_lock(&lock);
bool res = (in != out);
pthread_mutex_unlock(&lock);
return res;
}
四:迷宫问题
1:迷宫问题
普通的迷宫问题,使用栈来求得入口到出口的一条路径,并不保证该路径是最短的。思路如下:
将入口入栈;
置入口为“已走过”
while(栈非空)
元素出栈
根据该元素的当前位置以及当前方向,依次从RIGHT,DOWN,LEFT,UP顺时针方向,分别求得该元素在某个个方向上的下一个位置
如果4个位置中有一个位置是可通过的话:
将刚才出栈的元素重新入栈
将下一个位置的元素入栈
置下一个位置为“已走过”
最后,如果栈非空,则栈中的元素就代表了一条路径。
代码:
typedef struct
{
int x;
int y;
}pos;
#define XLEN 10
#define YLEN 10
int mz[XLEN][YLEN] =
{-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
-1, 0, 0,-1, 0, 0, 0,-1, 0,-1,
-1, 0, 0,-1, 0, 0, 0,-1, 0,-1,
-1, 0, 0, 0, 0,-1,-1, 0, 0,-1,
-1, 0,-1,-1,-1, 0, 0, 0, 0,-1,
-1, 0, 0, 0,-1, 0, 0, 0, 0,-1,
-1, 0,-1, 0, 0, 0,-1, 0, 0,-1,
-1, 0,-1,-1,-1, 0,-1,-1, 0,-1,
-1,-1, 0, 0, 0, 0, 0, 0, 0,-1,
-1,-1,-1,-1,-1,-1,-1,-1,-1,-1};
enum DI
{
RIGHT,
DOWN,
LEFT,
UP
};
Stack path;
int canpass(pos p)
{
if(p.y < 0 || p.y >= YLEN)return false;
if(p.x < 0 || p.x >= XLEN)return false;
if(mz[p.y][p.x] != 0)
{
return false;
}
return true;
}
pos nextpos(pos cur, int di)
{
pos next = cur;
switch(di)
{
case RIGHT:
{
next.x++;
return next;
}
case DOWN:
{
next.y++;
return next;
}
case LEFT:
{
next.x--;
return next;
}
case UP:
{
next.y--;
return next;
}
default:
{
next.x = -1;
next.y = -1;
return next;
}
}
}
inline int samepos(pos a, pos b)
{
if(a.x == b.x && a.y == b.y)return true;
return false;
}
void inline setmz(pos p)
{
mz[p.y][p.x] = 1;
}
void printpath(Stack *ptr, Stack *pres)
{
SElemType e;
while(! isStackEmpty(ptr))
{
pop(ptr, &e);
push(pres, e);
}
while(! isStackEmpty(pres))
{
pop(pres, &e);
if(! isStackEmpty(pres))
{
printf("%d(%d %d)->", e.ord, e.seat.x, e.seat.y);
}
else
{
printf("%d(%d %d)\n", e.ord, e.seat.x, e.seat.y);
}
}
}
void mazepath(pos begin, pos end)
{
Stack *ptr = &path;
Stack res;
Stack *pres = &res;
SElemType e = {}, nexte = {};
InitStack(ptr);InitStack(pres);
if(canpass(begin) == false || canpass(end) == false)
{
printf("error begin(%d %d) or end(%d %d) pos\n", begin.x, begin.y, end.x, end.y);
return;
}
e.di = -1;
e.ord = 1;
e.seat = begin;
push(ptr, e);
setmz(e.seat);
while(isStackEmpty(ptr) == false)
{
pop(ptr, &e);
if(samepos(e.seat, end))
{
push(ptr, e);
printpath(ptr, pres);
break;
}
int i;
pos npos;
for(i = 1; e.di+i <= UP; i++)
{
npos = nextpos(e.seat, e.di+i);
if(canpass(npos) == true)
{
e.di += i;
push(ptr, e);
nexte.di = -1;
nexte.seat = npos;
nexte.ord = e.ord+1;
push(ptr, nexte);
setmz(nexte.seat);
break;
}
}
}
}
2:迷宫问题的最短路径
为了求得从入口到出口的最短路径,使用队列,队列中的元素分别标识了该元素的当前位置,以及路径上父元素的位置,思路如下:
将入口入队列;
置入口为“已走过”
while(队列非空,且尚未找到最短路径)
元素出队列(元素还在内存中,只是队列的头指针+1)
根据该元素的当前位置,依次从RIGHT,DOWN,LEFT,UP顺时针方向,分别求得该元素在某个个方向上的下一个位置;
4个位置中凡是可以通过的,则将该位置入队列,且置该位置为“已通过”
如果4个位置中有出口,则找到了最短路径
如果找到了最短路径
从队列中最后一个元素开始往前,根据每个元素的父节点的位置信息,找到队列中的父元素。
代码:
void shortestmaze(pos begin, pos end)
{
if(iscanpass(begin) == false || iscanpass(end) == false)
{
printf("error begin(%d %d) or end(%d %d) pos\n", begin.x, begin.y, end.x, end.y);
return;
}
SqQueue sq;
SqQueue *ptr = &sq;
InitQueue(ptr);
QElemType e;
e.curpos = begin;
EnQueue(ptr, e);
mz[begin.y][begin.x] = 1;
pos cp;
pos np;
int findflag = false;
while(QueueLength(ptr) > 0 && findflag == false)
{
DeQueue(ptr, &e);
cp = e.curpos;
int i;
for(i = RIGHT; i <= UP; i++)
{
np = nextpos(cp, i);
if(iscanpass(np) == true)
{
e.parentpos = cp;
e.curpos = np;
mz[np.y][np.x] = 1;
EnQueue(ptr, e);
if(samepos(np, end))
{
findflag = true;
break;
}
}
}
}
QElemType *p = sq.base;
if(findflag == true)
{
int index = sq.rear-1;
printf("(%d %d)<-", p[index].curpos.x, p[index].curpos.y);
pos prepos = p[index].parentpos;
index--;
while(index >= 0)
{
if(samepos(prepos, p[index].curpos) == true)
{
if(index != 0)
{
printf("(%d %d)<-", p[index].curpos.x, p[index].curpos.y);
}
else
{
printf("(%d %d)\n", p[index].curpos.x, p[index].curpos.y);
}
prepos = p[index].parentpos;
}
index--;
}
}
else
{
printf("can not find shortest path\n");
}
}
五:树与二叉树
1:概念及性质:
满二叉树,完全二叉树因数据结构和算法导论定义不同,国内以下列定义为准(以数据结构为准):
满二叉树:除叶子结点外的所有结点均有两个子结点。节点数达到最大值。所有叶子结点必须在同一层上.|
完全二叉树:若设二叉树的深度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第 h 层所有的结点都连续集中在最左边,这就是完全二叉树。完全二叉树是由满二叉树而引出来的。
对任何一个二叉树,如果其叶子结点数为
,度为2的结点数为
则:
。
证明:假设度为1的节点数为
,所以n =
。假设数中边的数目为b,除了根节点之外,边与结点是一一对应的,所以:
n = b + 1。又因为b =
。所以:
=
。所以:
。
顺序存储结构适合于完全二叉树的存储。其他的一般是链式存储结构。
2:二叉树的遍历:
二叉树是一种非线性的数据结构,在对它进行操作时,总是需要逐一对每个数据元素实施操作,这样就存在一个操作顺序问题,由此提出了二叉树的遍历操作。所谓遍历二叉树就是按某种顺序访问二叉树中的每个结点一次且仅一次的过程。这里的访问可以是输出、比较、更新、查看元素内容等等各种操作。
二叉树的遍历方式分为两大类:一类按根、左子树和右子树三个部分进行访问(深度优先);另一类按层次访问(广度优先)。下面我们将分别进行讨论。
2.1、 按根、左子树和右子树三部分进行遍历
遍历二叉树的顺序存在下面6种可能:
TLR(根左右), TRL(根右左)
LTR(左根右), RTL(右根左)
LRT(左右根), RLT(右左根)
其中,TRL、RTL和RLT三种顺序在左右子树之间均是先右子树后左子树,这与人们先左后右的习惯不同,因此,往往不予采用。余下的三种顺序TLR、LTR和LRT根据根访问的位置不同分别被称为先序遍历、中序遍历和后序遍历。
(1)先序遍历
若二叉树为空,则结束遍历操作;否则
访问根结点;
先序遍历左子树;
先序遍历右子树。
(2)中序遍历若二叉树为空,则结束遍历操作;否则
中序遍历左子树;
访问根结点;
中序遍历右子树。
(3)后序遍历
若二叉树为空,则结束遍历操作;否则
后序遍历左子树;
后序遍历右子树;
访问根结点。
例如。以下是一棵二叉树及其经过三种遍历所得到的相应遍历序列
由此可以看出:(1)遍历操作实际上是将非线性结构线性化的过程,其结果为线性序列,并根据采用的遍历顺序分别称为先序序列、中序序列或后序序列;(2)遍历操作是一个递归的过程,因此,这三种遍历操作的算法可以用递归函数实现:
先序遍历递归算法
void PreOrder(BTree BT)
{
if (BT)
{
Visit(BT);
PreOrder(BT->Lchild);
PreOrder(BT->Rchild);
}
}
中序遍历递归算法
void InOrder(BTree BT)
{
if (BT)
{
InOrder(BT->Lchild);
Visit(BT);
InOrder(BT->Rchild);
}
}
后序遍历递归算法
void PostOrder(BTree BT)
{
if (BT)
{
PostOrder(BT->Lchild);
PostOrder(BT->Rchild);
Visit(BT);
}
}
二叉树的深度优先遍历的非递归的通用做法是采用栈,栈是实现递归的最常用的结构,利用一个栈来记下尚待遍历的结点或子树,以备以后访问,可以将递归的深度优先遍历改为非递归的算法。
先序遍历。将根节点入栈,考察当前节点(即栈顶节点),先访问当前节点,然后将其出栈(已经访问过,不再需要保留),然后先将其右孩子入栈,再将其左孩子入栈(这个顺序是为了让左孩子位于右孩子上面,以便左孩子的访问先于右孩子;当然如果某个孩子为空,就不用入栈了)。如果栈非空就重复上述过程直到栈空为止,结束算法。
void PreOrderNoRecursion(TreePtr p)
{
stack<TreeNode> stk;
TreeNode t = *p;
stk.push(t);
while (!stk.empty())
{
t = stk.top();
stk.pop();
cout<<t.data<<"";
if (t.right != NULL)
{
stk.push((*t.right));
}
if (t.left != NULL)
{
stk.push((*t.left));
}
}
}
中序遍历。将根节点入栈,考察当前节点(即栈顶节点),如果其有左孩子并且未被访问过(有标记),则将其左孩子入栈,否则访问当前节点并将其出栈,如果它有右孩子的话将右孩子入栈。如果栈非空就重复上述过程直到栈空为止,结束算法。中序遍历需要一个判断左孩子是否已经访问过了的标志。
voidInOrderNoRecursion(TreePtr p)
{
stack<TreeNode> stk;
TreeNode t = *p;
stk.push(t);
while (!stk.empty())
{
t = stk.top();
if (t.left != NULL && t.leftflag== false)
{
stk.push(*(t.left));
t.leftflag = true;
}
else
{
stk.pop();
cout<<t.data<<"";
if (t.right != NULL)
{
stk.push(*(t.right));
}
}
}
}
后序遍历。将根节点入栈,考察当前节点(即栈顶节点),如果其有左孩子并且左孩子未被访问过,则将其左孩子入栈,否则如果其有右孩子并且右孩子未被访问过,则将其右孩子入栈,如果都已经访问过,则访问其自身,并将其出栈。如果栈非空就重复上述过程直到栈空为止,结束算法。后序遍历需要两个标志,分别判断左孩子以及右孩子是否已经访问过了,实际编程中可以通过一个字段实现。
voidPostOrderNoRecursion(TreePtr p)
{
stack<TreeNode> stk;
TreeNode t = *p;
stk.push(t);
while (!stk.empty())
{
t = stk.top();
if (t.left != NULL &&t.leftflag == false)
{
t.leftflag == true;
stk.push(*(t.left));
}
else if (t.right != NULL && t. right flag== false)
{
t. right flag == true;
stk.push(*(t.right));
}
else
{
cout<<stk.top().data<<" ";
stk.pop();
}
}
}
根据一棵树的先序遍历和中序遍历,或者后序遍历和中序遍历序列,都可以唯一地确定一棵树。如果只知道先序和后序遍历,则不能唯一确定一棵树。
2.2 、按层次遍历二叉树
实现方法为从上层到下层,每层中从左侧到右侧依次访问每个结点。下面我们将给出一棵二叉树及其按层次顺序访问其中每个结点的遍历序列。
(1)顺序存储时,按层遍历的算法实现:
void LevelOreder(QBTreeBT)
{
for (i=1;i<=BT.n;i++)
if (BT.elem[i]!='#')Visite(BT.elem[i]);
}
(2)二叉树用链式存储结构表示时,按层遍历的算法实现:
在这个算法中,应使用一个队列结构完成这项操作。所谓记录访问结点就是入队操作,而取出记录的结点就是出队操作。这样一来,我们的算法就可以描述成下列形式:
访问根结点,并将根结点入队;
当队列不空时,重复下列操作:
从队列退出一个结点;
若其有左孩子,则访问左孩子,并将其左孩子入队;
若其有右孩子,则访问右孩子,并将其右孩子入队;
voidLevelOrder(BTree *BT)
{
if (!BT) exit;
InitQueue(Q); p=BT; //初始化
Visite(p); EnQueue(&Q,p); //访问根结点,并将根结点入队
while (!QueueEmpty(Q))
{
//当队非空时重复执行下列操作
DeQueue(&Q,&p); //出队
if (!p->Lchild)
{Visite(p->Lchild);EnQueue(&Q,p->Lchild);}//处理左孩子
if (!p->Rchild)
{Visite(p->Rchild);EnQueue(&Q,p->Rchild);}//处理右孩子
}
}
3:典型二叉树的操作算法
3.1、 输入一个二叉树的先序序列,构造这棵二叉树
为了保证唯一地构造出所希望的二叉树,在键入这棵树的先序序列时,需要在所有空二叉树的位置上填补一个特殊的字符,比如,'#'。在算法中,需要对每个输入的字符进行判断,如果对应的字符是'#',则在相应的位置上构造一棵空二叉树;否则,创建一个新结点。整个算法结构以先序遍历递归算法为基础,二叉树中结点之间的指针连接是通过指针参数在递归调用返回时完成。
算法:
BTreePre_Create_BT( )
{
getch(ch);
if (ch=='#') return NULL; //构造空树
else
{
BT=(BTree)malloc(sizeof(BTLinklist));//构造新结点
BT->data=ch;
BT->lchild =Pre_Create_BT( );//构造左子树
BT->rchild =Pre_Create_BT( );//构造右子树
return BT;
}
}
3.2、 计算一棵二叉树的叶子结点数目
这个操作可以使用三种遍历顺序中的任何一种,只是需要将访问操作变成判断该结点是否为叶子结点,如果是叶子结点将累加器加1即可。下面这个算法是利用中序遍历实现的。
算法:
void Leaf(BTreeBT, int *count)
{
if (BT)
{
Leaf(BT->child,&count); //计算左子树的叶子结点个数
if(BT->lchild==NULL&&BT->rchild==NULL) (*count)++;
Leaf(BT->rchild,&count); //计算右子树的叶子结点个数
}
}
3.3、 交换二叉树的左右子树
许多操作可以利用三种遍历顺序的任何一种,只是某种遍历顺序实现起来更加方便一些。而有些操作则不然,它只能使用其中的一种或两种遍历顺序。将二叉树中所有结点的左右子树进行交换这个操作就属于这类情况。该操作需要使用后续遍历
算法:
void change_left_right(BTree BT)
{
if (BT)
{
change_left_right(BT->lchild);
change_left_right(BT->rchild);
BT->lchild<->BT->rchild;
}
}
3.4 、求二叉树的高度
这个操作使用后序遍历比较符合人们求解二叉树高度的思维方式。首先分别求出左右子树的高度,在此基础上得出该棵树的高度,即左右子树较大的高度值加1。
算法:
int hight(BTreeBT)
{
//h1和h2分别是以BT为根的左右子树的高度
if (BT==NULL) return 0;
else
{
h1=hight(BT->lchild);
h2=hight(BT->right);
return max{h1,h2}+1;
}
}
六:树的存储结构及与二叉树的转换
1:树的存储结构
在计算机中,树的存储有多种方式,既可以采用顺序存储结构,也可以采用链式存储结构,但无论采用何种存储方式,都要求存储结构不但能存储各结点本身的数据信息,还要能唯一地反映树中各结点之间的逻辑关系。下面介绍几种基本的树的存储方式。
1.1、双亲表示法
用一组连续的存储空间(一维数组)存储树中的各个结点,数组中的一个元素表示树中的一个结点,数组元素为结构体类型,其中包括结点本身的信息以及结点的双亲结点在数组中的序号,树的这种存储方法称为双亲表示法。其存储表示可描述为:
数组结点结构:
typedef struct PTNode
{
TElemType data;
int parent; // 双亲位置域
} PTNode;
树结构:
typedef struct
{
PTNode nodes[MAX_TREE_SIZE];
int n; // 根结点的位置和结点个数
} PTree;
如下图:
树的双亲表示法对于实现Parent(t,x)操作和Root(x)操作很方便,但若求某结点的孩子结点,即实现Child(t,x,i)操作时,则需要查询整个数组。此外,这种存储方式不能反映各兄弟结点之间的关系,所以实现RightSibling(t,x)操作也比较困难。
1.2、孩子链表表示法
孩子链表法是将树按如下图所示的形式存储。其主体是一个与结点个数一样大小的一维数组,数组的每一个元素有两个域组成,一个域用来存放结点信息,另一个用来存放指针,该指针指向由该结点孩子组成的单链表的首位置。单链表的结构也由两个域组成,一个存放孩子结点在一维数组中的序号,另一个是指针域,指向下一个孩子。
孩子结点结构:
typedef struct CTNode
{
int child;
structCTNode *next;
}*ChildPtr;
数组结点结构:
typedef struct
{
TElemtypedata;
ChildPtrfirstchild; // 孩子链的头指针
}CTBox;
树结构:
typedef struct
{
CTBoxnodes[MAX_TREE_SIZE];
int n, r;// 结点数和根结点的位置
}CTree;
如下图:
在孩子表示法中查找双亲比较困难,查找孩子却十分方便,故适用于对孩子操作多的应用。
1.3、双亲孩子表示法
双亲孩子表示法是将双亲表示法和孩子表示法相结合的结果。其仍将各结点的孩子结点分别组成单链表,同时用一维数组顺序存储树中的各结点,数组元素除了包括结点本身的信息和该结点的孩子结点链表的头指针之外,还增设一个域,存储该结点双亲结点在数组中的序号。
如下图:
4、孩子兄弟表示法(树的二叉链表存储表示法)
这是一种常用的存储结构。其方法是这样的:在树中,每个结点除其信息域外,再增加两个分别指向该结点的第一个孩子结点和下一个兄弟结点的指针。在这种存储结构下,树中结点的存储表示可描述为:
typedefstruct CSNode
{
TElemtype data;
struct CSNode*firstchild, *nextsibling;
}CSNode, *CSTree;
如下图:
2:森林与二叉树的转换
从树的孩子兄弟表示法可以看到,二叉树与树都可用二叉链表作为存储结构,则以二叉链表作为媒介可导出树与二叉树之间的一个对应关系。也就是说给定一棵树,可以找到唯一的一棵二叉树与之对应。
如下图:
从树的二叉链表表示的定义可知,任何一棵树对应的二叉树的根节点的右子树必空。若把第二棵树的根节点看成是第一棵树的根节点的兄弟,则同样可导出森林与二叉树的对应关系。
如下图:
由树结构的定义可引出两种次序遍历树的方法,一种是先根遍历树:先访问树的根节点,然后依次先根遍历根的每棵子树;另一种是后根遍历:先依次后根遍历每棵子树,然后访问根节点。
当以二叉链表作树的存储结构时,树的先根遍历和后根遍历可借用二叉树的先序遍历和中序遍历的算法实现。
七:哈夫曼树及哈夫曼编码
1.树的路径长度
树的路径长度是从树根到树中每一结点的路径长度之和。在结点数目相同的二叉树中,完全二叉树的路径长度最短。
2.树的带权路径长度(Weighted Path Length of Tree,简记为WPL)
结点的权:在一些应用中,赋予树中结点的一个有某种意义的实数。
结点的带权路径长度:结点到树根之间的路径长度与该结点上权的乘积。
树的带权路径长度(Weighted Path Length of Tree):定义为树中所有叶结点的带权路径长度之和,通常记为:
其中:
n表示叶子结点的数目
wi和li分别表示叶结点ki的权值和根到结点ki之间的路径长度。
树的带权路径长度亦称为树的代价。
3.最优二叉树或哈夫曼树
在权为wl,w2,…,wn的n个叶子所构成的所有二叉树中,带权路径长度最小(即代价最小)的二叉树称为最优二叉树或哈夫曼树。
【例】给定4个叶子结点a,b,c和d,分别带权7,5,2和4。构造如下图所示的三棵二叉树(还有许多棵):
它们的带权路径长度分别为:
(a)WPL=7*2+5*2+2*2+4*2=36
(b)WPL=7*3+5*3+2*1+4*2=46
(c)WPL=7*1+5*2+2*3+4*3=35
其中(c)树的WPL最小,可以验证,它就是哈夫曼树。
注意:
① 叶子上的权值均相同时,完全二叉树一定是最优二叉树,否则完全二叉树不一定是最优二叉树。
② 最优二叉树中,权越大的叶子离根越近。
③最优二叉树的形态不唯一,WPL最小
4:构造最优二叉树
4.1.哈夫曼算法
哈夫曼首先给出了对于给定的叶子数目及其权值构造最优二叉树的方法,故称其为哈夫曼算法。其基本思想是:
(1)根据给定的n个权值wl,w2,…,wn构成n棵二叉树的森林F={T1,T2,…,Tn},其中每棵二叉树Ti中都只有一个权值为wi的根结点,其左右子树均空。
(2)在森林F中选出两棵根结点权值最小的树(当这样的树不止两棵树时,可以从中任选两棵),将这两棵树合并成一棵新树,为了保证新树仍是二叉树,需要增加一个新结点作为新树的根,并将所选的两棵树的根分别作为新根的左右孩子(谁左,谁右无关紧要),将这两个孩子的权值之和作为新树根的权值。
(3)对新的森林F重复(2),直到森林F中只剩下一棵树为止。这棵树便是哈夫曼树。
注意:
① 初始森林中的n棵二叉树,每棵树是一个孤立的结点,它们既是根,又是叶子
② n个叶子的哈夫曼树要经过n-1次合并,产生n-1个新结点。最终求得的哈夫曼树中共有2n-1个结点。
③ 哈夫曼树是严格的二叉树,没有度数为1的分支结点。
4.2.代码实现
HMnode*huffmantree = NULL;
ints1 = -1, s2 = -1;
m= 2 * n - 1;
huffmantree= (HMnode *)malloc(m * sizeof(HMnode));
for(i= 0; i < m; i++)
{
if(i< n)
{
huffmantree[i].weight= begin[i];//先复制叶子结点的权重
}
else
{
huffmantree[i].weight= -1;
}
huffmantree[i].parent= -1;
huffmantree[i].lchild= -1;
huffmantree[i].rchild= -1;
}
intj = 0;
for(i= n; i < m; i++)
{
selectsmall(huffmantree,i,&s1,&s2);
huffmantree[i].weight= huffmantree[s1].weight + huffmantree[s2].weight;
huffmantree[s1].parent= i;
huffmantree[s2].parent= i;
huffmantree[i].lchild= s1;
huffmantree[i].rchild= s2;
}
5:编码方案
给定的字符集C,可能存在多种编码方案。
1:等长编码方案
等长编码方案将给定字符集C中每个字符的码长定为[lg|C|],|C|表示字符集的大小。
2:变长编码方案
变长编码方案将频度高的字符编码设置短,将频度低的字符编码设置较长。变长编码可能使解码产生二义性。产生该问题的原因是某些字符的编码可能与其他字符的编码开始部分(称为前缀)相同。
比如:设E、T、W分别编码为00、01、0001,则解码时无法确定信息串0001是ET还是W。
3:前缀码方案
对字符集进行编码时,要求字符集中任一字符的编码都不是其它字符的编码的前缀,这种编码称为前缀(编)码。等长编码是前缀码
4:最优前缀码
平均码长或文件总长最小的前缀编码称为最优的前缀码。最优的前缀码对文件的压缩效果亦最佳。
6:根据最优二叉树构造哈夫曼编码
利用哈夫曼树很容易求出给定字符集及其概率(或频度)分布的最优前缀码。哈夫曼编码正是一种应用广泛且非常有效的数据压缩技术。该技术一般可将数据文件压缩掉20%至90%,其压缩效率取决于被压缩文件的特征。
6.1:具体做法
用字符ci作为叶子,频率pi做为叶子ci的权,构造一棵哈夫曼树,并将树中左分支和右分支分别标记为0和1;
将从根到叶子的路径上的标号依次相连,作为该叶子所表示字符的编码。该编码即为最优前缀码(也称哈夫曼编码)。
6.2:哈夫曼编码为最优前缀码
由哈夫曼树求得编码为最优前缀码的原因:
每个叶子字符ci的码长恰为从根到该叶子的路径长度li,平均码长(或文件总长)又是二叉树的带权路径长度WPL。而哈夫曼树是WPL最小的二叉树,因此编码的平均码长(或文件总长)亦最小。
树中没有一片叶子是另一叶子的祖先,每片叶子对应的编码就不可能是其它叶子编码的前缀。即上述编码是二进制的前缀码。
6.3:求哈夫曼编码的算法
(1)思想方法
给定字符集的哈夫曼树生成后,求哈夫曼编码的具体实现过程是:依次以叶子T[i](0≤i≤n-1)为出发点,向上回溯至根为止。上溯时走左分支则生成代码0,走右分支则生成代码1。
注意:
①由于生成的编码与要求的编码反序,将生成的代码先从后往前依次存放在一个临时向量中,并设一个指针start指示编码在该向量中的起始位置(start初始时指示向量的结束位置)。
②当某字符编码完成时,从临时向量的start处将编码复制到该字符相应的位串bits中即可。
③ 因为字符集大小为n,故变长编码的长度不会超过n,加上一个结束符'\0',bits的大小应为n。
(2)代码:
int m = 2 * size - 1;
HMnode*tree = NULL;
tree= buildhuffman(set, size);
char**code = malloc(size * sizeof(char* ));//存放n个结点的编码
inti, j, k;
intstart;
for(i= 0; i < size; i++)
{
code[i]= malloc(size);
memset(code[i],0, size);
}
char*tempcode = malloc(size);
for(i= 0; i < size; i++)
{
memset(tempcode,0, size);
start= size - 2;//从叶子结点到根,因而是从后往前赋值
k= i;
j= tree[k].parent;
while(j!= -1)
{
if(tree[j].lchild== k)
{
tempcode[start--]= '0';
}
else
{
tempcode[start--]= '1';
}
k= j;
j= tree[k].parent;
}
memcpy(code[i],&(tempcode[start+1]), size-1-start);
printf("%d->%s\n",tree[i].weight, code[i]);
}
1:数据结构就是一种动态的可变集合,不同的算法对动态集合有不同的操作,支持插入,删除,测试元素是否属于集合这些操作的动态集合成为字典。集合中的对象,一般都有关键字,有的对象还有卫星数据。
动态集合上的操作可以分为:查询操作和修改操作,任何具体应用通常只会进行下面操作的若干个:搜索,插入,删除,最小值,最大值,前继,后继。
2:栈、队列、链表
栈是先进后出,队列是先进先出。二者既可以用数组实现,也可以用链表实现。
链表可以有很多形式,可以是单向的或者双向的,可以是有序的或者无序的,也可以是循环的。
3:无指针情况下的实现。
有些语言不支持指针,需要其他的方法来实现链式数据结构,比如数组:
下图说明了如何用三个数组表示链表,三个数组分别代表了key,next,prev。三个数组项key[x],next[x],prev[x]一起表示链表中的一个对象。
也可以使用一维数组来实现,数组中每三个元素表示链表中的一个对象,比如下图:
4:有根树
链式数据结构除了表示链表之外,还可以表示有根树,比如二叉树,每个结点包含父指针,左孩子,右孩子,如下图:
二叉树的表示方法可以推广到每个结点的孩子数最多为常数k的任意类型的树:只需要将left,right用child1,child2,…,childk代替即可。但是当孩子的节点数无限制的时候,这种方法就失效了。
可以使用左孩子右兄弟表示法,此种方法只需要O(n)的存储空间。这种方法中,每个结点中包含,父指针p,x.leftchild指向结点x最左边的孩子,x.rightsibling指向x右侧相邻的兄弟结点。如下图:
二;栈
1:栈的顺序表示:
一般来说,在初始化空栈时不应限定栈的最大容量,较合理的做法是:先为栈分配一个基本容量,然后在程序中,当栈的空间不够使用时在逐渐扩大。base表示栈底,top表示栈顶。当top=base时,表示栈空。每当插入新的栈顶元素时,top增加1,删除栈顶元素时,top减少1.所以,非空栈中的top指针始终在栈顶元素的下一个位置上。如下图:
栈的实现:
void InitStack(Stack *s)
{
s->base = (SElemType *)malloc(STACK_INIT_SIZE * sizeof(SElemType));
if(s->base == NULL)
{
printf("Init Stack mallocerror\n");
return;
}
s->top = s->base;
s->stacksize = STACK_INIT_SIZE;
}
int isStackEmpty(Stack *s)
{
if(s->top ==s->base)
{
//printf("the stack isempty\n");
return true;
}
return false;
}
int stacklen(Stack *s)
{
return s->top - s->base;
}
void GetTop(Stack*s, SElemType *e)
{
if(s->top == s->base)
{
printf("the stack isempty\n");
return;
}
*e = *(s->top - 1);
}
void push(Stack *s, SElemType e)
{
if(s->top - s->base >=s->stacksize)
{
s->base = (SElemType*)realloc(s->base,
(s->stacksize+ STACKINCREMENT) * sizeof(SElemType));
if(s->base == NULL)
{
printf("increase Stackerror\n");
return;
}
s->top = s->base + s->stacksize;
s->stacksize += STACKINCREMENT;
}
*(s->top ++) = e;
}
void pop(Stack *s, SElemType *e)
{
if(s->top == s->base)
{
printf("the stack isempty\n");
return;
}
*e = *(--(s->top));
}
2:迷宫求解
求迷宫中从入口到出口的所有路径是一个经典的程序设计问题。由于计算机解迷宫时,通常用“穷举求解”的方法,即从入口出发,顺某一方向向前搜索,若能走通,则继续向前走,否则原路退回,换一个方向在继续探索,直到所有的通路都探索到为止。为了保证在任何位置上都能够沿原路退回,需要栈来保存当前路径。
可以用二维数组表示迷宫,其中1表示墙,0表示通路:
maze mz = { 1,1,1,1,1,1,1,1,1,1,
1,0,0,1,0,0,0,1,0,1,
1,0,0,1,0,0,0,1,0,1,
1,0,0,0,0,1,1,0,0,1,
1,0,1,1,1,0,0,0,0,1,
1,0,0,0,1,0,0,0,0,1,
1,0,1,0,0,0,1,0,0,1,
1,0,1,1,1,0,1,1,0,1,
1,1,0,0,0,0,0,0,0,1,
1,1,1,1,1,1,1,1,1,1};
代码参见《迷宫问题》。
3:表达式求值
表达式求值是栈应用的又一个典型例子。这里介绍一种简单直观、广为使用的算法,通常称为“算符优先法”。
要对下面的算术表达式求值:4 + 2 × 3 - 10/5
首先要了解算术四则运算的规则。即:
(1)先乘除,后加减;
(2)从左算到右;
(3)先括号内,后括号外。
由此,这个算术表达式的计算顺序应为
4 + 2 × 3 - 10/5 = 4 + 6 - 10/5 = 10 - 10/5 = 10 - 2 =8
算符优先法就是根据这个运算优先关系的规定来实现对表达式的编译或解释执行的。
任何一个表达式都是由操作数(operand)、运算符(operator)和界限符(delimiter)组成的,为了叙述的简洁,我们仅讨论简单算术表达式的求值问题。这种表达式只含加、减、乘、除4种运算符。
我们把运算符和界限符统称为算符,它们构成的集合命名为OP。根据上述3条运算规则,在运算的每一步中,任意两个相继出现的算符θ1和θ2之间的优先关系至多是下面3种关系之一:
θ1<θ2 θ1的优先权低于θ2
θ1=θ2 θ1的优先权等于θ2
θ1>θ2 θ1的优先权高于θ2
表1定义了算符之间的这种优先关系:
由规则(3),+、-、*和/为θ1时的优先性均低于“(”但高于“)”,由规则(2),当θ1=θ2时,令θ1>θ2,“#”是表达式的结束符。为了算法简洁,在表达式的最左边也虚设一个“#”构成整个表达式的一对括号。表中的“(”=“)”表示当左右括号相遇时,括号内的运算已经完成。同理,“#”=“#”表示整个表达式求值完毕。“)”与“(”、“#”与“)”以及“(”与“#”之间无优先关系,这是因为表达式中不允许它们相继出现,一旦遇到这种情况,则可以认为出现了语法错误。在下面的讨论中,我们暂假定所输入的表达式不会出现语法错误。
为实现算符优先算法,可以使用两个工作栈。一个称做OPTR,用以寄存运算符;另一个称做OPND,用以寄存操作数或运算结果。算法的基本思想的:
(1)首先置操作数栈为空栈,表达式起始符“#”为运算符栈的栈底元素;
(2)依次读入表达式中每个字符,若是操作数则进 OPND栈,若是运算符则和OPTR栈的栈顶运算符比较优先权后作相应操作,直至整个表达式求值完毕(即
OPTR栈的栈顶元素和当前读入的字符均为“#”)。
以下算法描述了这个求值过程。
OperandType EvaluateExpression()
{
// 算术表达式求值的算符优先算法。设 OPTR 和 OPND 分别为运算符栈和运算数栈,OP 为运算符集合。
InitStack(OPTR);Push(OPTR,'#');
InitStack(OPND);c = getchar();
while(c!='#' || GetTop(OPTR)!='#')
{
if(!In(c,OP))
{ Push((OPND,c); c = getchar(); } // 不是运算符则进栈
else
switch(Precede(GetTop(OPTR),c))
{
case '<': // 栈顶元素优先权低
Push(OPTR,c); c = getchar();
break;
case '=': // 脱括号并接收下一字符
Pop(OPTR,x); c = getchar();
break;
case'>': // 退栈并将运算结果入栈
Pop(OPTR,theta);
Pop(OPND,b); Pop(OPND,a);
Push(OPND,Operate(a,theta,b));
break;
}// switch
}// while
return GetTop(OPND);
}// EvaluateExpression
3:汉诺塔问题:
void hanoi(intn,char x, char y, char z)
{
//将塔座x上按直径有小到大且自上而下编号为1至n的n个圆盘按规则搬动到
//塔座z上,y塔座可以用做辅助
if(n==1)
{
move(x,1,z);//当n==1时,则搬动x上的圆盘放到z上
}
else
{
hanoi(n-1,x,z,y);//将x上编号为1至n-1的圆盘移动到y,z做辅助塔
move(x,n,z);//执行搬动操作
hanoi(n-1,y,x,z);//将y塔座上的编号为1至n-1的圆盘移动到z上,x做辅助塔
}
}
三:队列
允许插入的一端叫对尾,允许删除的一端叫队头。
双端队列,限定插入和删除操作在表的两端进行的线性表,如下图:
和顺序栈相类似,在队列的顺序存储结构中,除了用一组地址连续的存储单元依次存放从队列头到队列尾的元素之外,尚需附设两个指针front和rear分别指向队列头元素和队列尾元素的位置。初始化建空队列时,令front=rear=0,每当插入新的队列尾元素时,“尾指针增1”;每当删除队列头元素时,“头指针增1”。因此,在非空队列中,头指针始终指向队列头元素,而尾指针始终指向队列尾元素的下一个位置。如图4所示。
图4 头、尾指针和队列中元素之间的关系
(a)空队列;(b)J1、J2和J3相继入队列;(c)J1和J2相继被删除;(d)J4、J5和J6相继插入队列之后J3及J4被删除
一个较巧妙的办法是将顺序队列臆造为一个环状的空间,如图5所示,称之为循环队列。指针和队列元素之间关系不变,如图6(a)所示循环队列中,队列头元素时J3,队列尾元素是J5,之后J6、J7和J8相继插入,则队列空间均被占满,如图6(b)所示,此时Q.front=Q.rear;反之,若J3、J4和J5相继从图6(a)的队列中删除,使队列呈“空”的状态,如图6(c)所示。此时亦存在关系式Q.front=Q.rear,由此可见,只凭等式Q.front=Q.rear无法判别队列空间是“空”还是“满”。可由两种处理方法:其一是另设一个标志位以区别队列是“空”还是“满”;其二是少用一个元素空间,约定以“队列头指针在队列尾指针的下一位置(指环状的下一位置)上”作为队列呈“满”状态的标志。
图5 循环队列示意图
图6 循环队列的头尾指针 (a)一般情况;(b)队列满时;(c)空队列;
1:循环队列实现(数组,空一个元素):
void InitQueue(SqQueue &Q)
{
Q.base = (QElemType *)malloc(MAXQSIZE * sizeof(QElemType));
if(!Q.base) exit (-1);
Q.front=Q.rear=0;
return;
}
int QueueLength(SqQueue Q)
{
return (Q.rear -Q.front + MAXQSIZE) % MAXQSIZE;
}
void EnQueue(SqQueue &Q, QElemType e)
{
if((Q.rear+1) %MAXQSIZE == Q.front)
{
printf("queue isfull\n");
return;
}
Q.base[Q.rear] = e;
Q.rear = (Q.rear+1) % MAXQSIZE;
return;
}
void DeQueue(SqQueue &Q, QElemType &e)
{
if(Q.front ==Q.rear)
{
printf("queue isempty\n");
return;
}
e = Q.base[Q.front];
Q.front = (Q.front+1) % MAXQSIZE;
return;
}
2:循环队列实现(标记法)
#define QUEUESIZE 10
//定义循环队列结构体
typedef struct _CycleQueue
{
int front;//队首
int rear;//队尾
int count;//队列中元素个数
int data[QUEUESIZE];//存储队列元素
} CycleQueue;
void InitQueue(CycleQueue *queue)
{
queue->front=0;
queue->rear=0;
queue->count=0;
}
int InQueue(CycleQueue *queue,intdata)
{
if (IsQueueFull(queue))
{
return 0;
}
else
{
queue->data[queue->rear]=data;
queue->count++;
queue->rear=(queue->rear+1)%QUEUESIZE;
return 1;
}
}
int OutQueue(CycleQueue *queue,int*pdata)
{
if (IsQueueEmpty(queue))
{
return 0;
}
else
{
*pdata=queue->data[queue->front];
queue->count--;
queue->front=(queue->front+1)%QUEUESIZE;
return 1;
}
}
int IsQueueEmpty(CycleQueue *queue)
{
return queue->count==0;
}
int IsQueueFull(CycleQueue *queue)
{
return queue->count==QUEUESIZE;
}
3:循环队列(多线程同步)
#define std_size 2000
#include<pthread.h>
typedef unsigned int uint;
template <classT>
class SyncFifo
{
protected:
uint in, out;
uint size;
T **tab;
pthread_mutex_t lock;
pthread_cond_t nonEmpty;
public:
/* Specific constructor */
SyncFifo (uint size = std_size);
/* Destructor */
~SyncFifo ();
/* get the first object */
T *get ();
/* get the first object (non totallyblocking)
* return NULL if there is none
*/
T *tryGet ();
/* add an object in the Fifo */
void put (T *obj);
/* how many itmes are there inside ? */
int getLength ();
bool isNonEmpty ();
void waitnoempty()
{
pthread_mutex_lock(&lock);
while(in == out)
{
pthread_cond_wait(&nonEmpty,&lock);
}
pthread_mutex_unlock(&lock);
}
};
template <classT>
SyncFifo<T>::SyncFifo(uint size)
{
tab = new T*[size];
this->size = size;
in = 0;
out = 0;
pthread_mutex_init (&lock, NULL);
pthread_cond_init (&nonEmpty, NULL);
}
template <classT>
SyncFifo<T>::~SyncFifo()
{
delete [] tab;
pthread_mutex_destroy (&lock);
pthread_cond_destroy (&nonEmpty);
}
template <classT>
T * SyncFifo<T>::get ()
{
T *tmp;
pthread_mutex_lock(&lock);
while(in == out)
{
pthread_cond_wait(&nonEmpty,&lock);
}
tmp = tab[out];
out = (out + 1) % size;
pthread_mutex_unlock(&lock);
return tmp;
}
template <classT>
T *SyncFifo<T>::tryGet ()
{
T *tmp = NULL;
pthread_mutex_lock(&lock);
if (in != out)
{
// The stack is not empty
tmp = tab[out];
out = (out + 1) % size;
}
pthread_mutex_unlock(&lock);
return tmp;
}
template <classT>
void SyncFifo<T>::put (T *obj)
{
pthread_mutex_lock(&lock);
tab[in] = obj;
if (in == out)
{
pthread_cond_broadcast(&nonEmpty);
}
in = (in + 1) % size;
if (in == out)
{
T **tmp;
tmp = new T*[2*size];
for (uint i=out; i<size; i++)
{
tmp[i]= tab[i];
}
for (uint i=0; i<in; i++)
{
tmp[i+size] = tab[i];
}
in += size;
size *= 2;
delete [] tab;
tab = tmp;
}
pthread_mutex_unlock(&lock);
}
template <classT>
int SyncFifo<T>::getLength ()
{
int tmp;
pthread_mutex_lock(&lock);
tmp = (in + size - out) % size;
pthread_mutex_unlock(&lock);
return tmp;
}
template <classT>
bool SyncFifo<T>::isNonEmpty ()
{
pthread_mutex_lock(&lock);
bool res = (in != out);
pthread_mutex_unlock(&lock);
return res;
}
四:迷宫问题
1:迷宫问题
普通的迷宫问题,使用栈来求得入口到出口的一条路径,并不保证该路径是最短的。思路如下:
将入口入栈;
置入口为“已走过”
while(栈非空)
元素出栈
根据该元素的当前位置以及当前方向,依次从RIGHT,DOWN,LEFT,UP顺时针方向,分别求得该元素在某个个方向上的下一个位置
如果4个位置中有一个位置是可通过的话:
将刚才出栈的元素重新入栈
将下一个位置的元素入栈
置下一个位置为“已走过”
最后,如果栈非空,则栈中的元素就代表了一条路径。
代码:
typedef struct
{
int x;
int y;
}pos;
#define XLEN 10
#define YLEN 10
int mz[XLEN][YLEN] =
{-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
-1, 0, 0,-1, 0, 0, 0,-1, 0,-1,
-1, 0, 0,-1, 0, 0, 0,-1, 0,-1,
-1, 0, 0, 0, 0,-1,-1, 0, 0,-1,
-1, 0,-1,-1,-1, 0, 0, 0, 0,-1,
-1, 0, 0, 0,-1, 0, 0, 0, 0,-1,
-1, 0,-1, 0, 0, 0,-1, 0, 0,-1,
-1, 0,-1,-1,-1, 0,-1,-1, 0,-1,
-1,-1, 0, 0, 0, 0, 0, 0, 0,-1,
-1,-1,-1,-1,-1,-1,-1,-1,-1,-1};
enum DI
{
RIGHT,
DOWN,
LEFT,
UP
};
Stack path;
int canpass(pos p)
{
if(p.y < 0 || p.y >= YLEN)return false;
if(p.x < 0 || p.x >= XLEN)return false;
if(mz[p.y][p.x] != 0)
{
return false;
}
return true;
}
pos nextpos(pos cur, int di)
{
pos next = cur;
switch(di)
{
case RIGHT:
{
next.x++;
return next;
}
case DOWN:
{
next.y++;
return next;
}
case LEFT:
{
next.x--;
return next;
}
case UP:
{
next.y--;
return next;
}
default:
{
next.x = -1;
next.y = -1;
return next;
}
}
}
inline int samepos(pos a, pos b)
{
if(a.x == b.x && a.y == b.y)return true;
return false;
}
void inline setmz(pos p)
{
mz[p.y][p.x] = 1;
}
void printpath(Stack *ptr, Stack *pres)
{
SElemType e;
while(! isStackEmpty(ptr))
{
pop(ptr, &e);
push(pres, e);
}
while(! isStackEmpty(pres))
{
pop(pres, &e);
if(! isStackEmpty(pres))
{
printf("%d(%d %d)->", e.ord, e.seat.x, e.seat.y);
}
else
{
printf("%d(%d %d)\n", e.ord, e.seat.x, e.seat.y);
}
}
}
void mazepath(pos begin, pos end)
{
Stack *ptr = &path;
Stack res;
Stack *pres = &res;
SElemType e = {}, nexte = {};
InitStack(ptr);InitStack(pres);
if(canpass(begin) == false || canpass(end) == false)
{
printf("error begin(%d %d) or end(%d %d) pos\n", begin.x, begin.y, end.x, end.y);
return;
}
e.di = -1;
e.ord = 1;
e.seat = begin;
push(ptr, e);
setmz(e.seat);
while(isStackEmpty(ptr) == false)
{
pop(ptr, &e);
if(samepos(e.seat, end))
{
push(ptr, e);
printpath(ptr, pres);
break;
}
int i;
pos npos;
for(i = 1; e.di+i <= UP; i++)
{
npos = nextpos(e.seat, e.di+i);
if(canpass(npos) == true)
{
e.di += i;
push(ptr, e);
nexte.di = -1;
nexte.seat = npos;
nexte.ord = e.ord+1;
push(ptr, nexte);
setmz(nexte.seat);
break;
}
}
}
}
2:迷宫问题的最短路径
为了求得从入口到出口的最短路径,使用队列,队列中的元素分别标识了该元素的当前位置,以及路径上父元素的位置,思路如下:
将入口入队列;
置入口为“已走过”
while(队列非空,且尚未找到最短路径)
元素出队列(元素还在内存中,只是队列的头指针+1)
根据该元素的当前位置,依次从RIGHT,DOWN,LEFT,UP顺时针方向,分别求得该元素在某个个方向上的下一个位置;
4个位置中凡是可以通过的,则将该位置入队列,且置该位置为“已通过”
如果4个位置中有出口,则找到了最短路径
如果找到了最短路径
从队列中最后一个元素开始往前,根据每个元素的父节点的位置信息,找到队列中的父元素。
代码:
void shortestmaze(pos begin, pos end)
{
if(iscanpass(begin) == false || iscanpass(end) == false)
{
printf("error begin(%d %d) or end(%d %d) pos\n", begin.x, begin.y, end.x, end.y);
return;
}
SqQueue sq;
SqQueue *ptr = &sq;
InitQueue(ptr);
QElemType e;
e.curpos = begin;
EnQueue(ptr, e);
mz[begin.y][begin.x] = 1;
pos cp;
pos np;
int findflag = false;
while(QueueLength(ptr) > 0 && findflag == false)
{
DeQueue(ptr, &e);
cp = e.curpos;
int i;
for(i = RIGHT; i <= UP; i++)
{
np = nextpos(cp, i);
if(iscanpass(np) == true)
{
e.parentpos = cp;
e.curpos = np;
mz[np.y][np.x] = 1;
EnQueue(ptr, e);
if(samepos(np, end))
{
findflag = true;
break;
}
}
}
}
QElemType *p = sq.base;
if(findflag == true)
{
int index = sq.rear-1;
printf("(%d %d)<-", p[index].curpos.x, p[index].curpos.y);
pos prepos = p[index].parentpos;
index--;
while(index >= 0)
{
if(samepos(prepos, p[index].curpos) == true)
{
if(index != 0)
{
printf("(%d %d)<-", p[index].curpos.x, p[index].curpos.y);
}
else
{
printf("(%d %d)\n", p[index].curpos.x, p[index].curpos.y);
}
prepos = p[index].parentpos;
}
index--;
}
}
else
{
printf("can not find shortest path\n");
}
}
五:树与二叉树
1:概念及性质:
满二叉树,完全二叉树因数据结构和算法导论定义不同,国内以下列定义为准(以数据结构为准):
满二叉树:除叶子结点外的所有结点均有两个子结点。节点数达到最大值。所有叶子结点必须在同一层上.|
完全二叉树:若设二叉树的深度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第 h 层所有的结点都连续集中在最左边,这就是完全二叉树。完全二叉树是由满二叉树而引出来的。
对任何一个二叉树,如果其叶子结点数为
,度为2的结点数为
则:
。
证明:假设度为1的节点数为
,所以n =
。假设数中边的数目为b,除了根节点之外,边与结点是一一对应的,所以:
n = b + 1。又因为b =
。所以:
=
。所以:
。
顺序存储结构适合于完全二叉树的存储。其他的一般是链式存储结构。
2:二叉树的遍历:
二叉树是一种非线性的数据结构,在对它进行操作时,总是需要逐一对每个数据元素实施操作,这样就存在一个操作顺序问题,由此提出了二叉树的遍历操作。所谓遍历二叉树就是按某种顺序访问二叉树中的每个结点一次且仅一次的过程。这里的访问可以是输出、比较、更新、查看元素内容等等各种操作。
二叉树的遍历方式分为两大类:一类按根、左子树和右子树三个部分进行访问(深度优先);另一类按层次访问(广度优先)。下面我们将分别进行讨论。
2.1、 按根、左子树和右子树三部分进行遍历
遍历二叉树的顺序存在下面6种可能:
TLR(根左右), TRL(根右左)
LTR(左根右), RTL(右根左)
LRT(左右根), RLT(右左根)
其中,TRL、RTL和RLT三种顺序在左右子树之间均是先右子树后左子树,这与人们先左后右的习惯不同,因此,往往不予采用。余下的三种顺序TLR、LTR和LRT根据根访问的位置不同分别被称为先序遍历、中序遍历和后序遍历。
(1)先序遍历
若二叉树为空,则结束遍历操作;否则
访问根结点;
先序遍历左子树;
先序遍历右子树。
(2)中序遍历若二叉树为空,则结束遍历操作;否则
中序遍历左子树;
访问根结点;
中序遍历右子树。
(3)后序遍历
若二叉树为空,则结束遍历操作;否则
后序遍历左子树;
后序遍历右子树;
访问根结点。
例如。以下是一棵二叉树及其经过三种遍历所得到的相应遍历序列
由此可以看出:(1)遍历操作实际上是将非线性结构线性化的过程,其结果为线性序列,并根据采用的遍历顺序分别称为先序序列、中序序列或后序序列;(2)遍历操作是一个递归的过程,因此,这三种遍历操作的算法可以用递归函数实现:
先序遍历递归算法
void PreOrder(BTree BT)
{
if (BT)
{
Visit(BT);
PreOrder(BT->Lchild);
PreOrder(BT->Rchild);
}
}
中序遍历递归算法
void InOrder(BTree BT)
{
if (BT)
{
InOrder(BT->Lchild);
Visit(BT);
InOrder(BT->Rchild);
}
}
后序遍历递归算法
void PostOrder(BTree BT)
{
if (BT)
{
PostOrder(BT->Lchild);
PostOrder(BT->Rchild);
Visit(BT);
}
}
二叉树的深度优先遍历的非递归的通用做法是采用栈,栈是实现递归的最常用的结构,利用一个栈来记下尚待遍历的结点或子树,以备以后访问,可以将递归的深度优先遍历改为非递归的算法。
先序遍历。将根节点入栈,考察当前节点(即栈顶节点),先访问当前节点,然后将其出栈(已经访问过,不再需要保留),然后先将其右孩子入栈,再将其左孩子入栈(这个顺序是为了让左孩子位于右孩子上面,以便左孩子的访问先于右孩子;当然如果某个孩子为空,就不用入栈了)。如果栈非空就重复上述过程直到栈空为止,结束算法。
void PreOrderNoRecursion(TreePtr p)
{
stack<TreeNode> stk;
TreeNode t = *p;
stk.push(t);
while (!stk.empty())
{
t = stk.top();
stk.pop();
cout<<t.data<<"";
if (t.right != NULL)
{
stk.push((*t.right));
}
if (t.left != NULL)
{
stk.push((*t.left));
}
}
}
中序遍历。将根节点入栈,考察当前节点(即栈顶节点),如果其有左孩子并且未被访问过(有标记),则将其左孩子入栈,否则访问当前节点并将其出栈,如果它有右孩子的话将右孩子入栈。如果栈非空就重复上述过程直到栈空为止,结束算法。中序遍历需要一个判断左孩子是否已经访问过了的标志。
voidInOrderNoRecursion(TreePtr p)
{
stack<TreeNode> stk;
TreeNode t = *p;
stk.push(t);
while (!stk.empty())
{
t = stk.top();
if (t.left != NULL && t.leftflag== false)
{
stk.push(*(t.left));
t.leftflag = true;
}
else
{
stk.pop();
cout<<t.data<<"";
if (t.right != NULL)
{
stk.push(*(t.right));
}
}
}
}
后序遍历。将根节点入栈,考察当前节点(即栈顶节点),如果其有左孩子并且左孩子未被访问过,则将其左孩子入栈,否则如果其有右孩子并且右孩子未被访问过,则将其右孩子入栈,如果都已经访问过,则访问其自身,并将其出栈。如果栈非空就重复上述过程直到栈空为止,结束算法。后序遍历需要两个标志,分别判断左孩子以及右孩子是否已经访问过了,实际编程中可以通过一个字段实现。
voidPostOrderNoRecursion(TreePtr p)
{
stack<TreeNode> stk;
TreeNode t = *p;
stk.push(t);
while (!stk.empty())
{
t = stk.top();
if (t.left != NULL &&t.leftflag == false)
{
t.leftflag == true;
stk.push(*(t.left));
}
else if (t.right != NULL && t. right flag== false)
{
t. right flag == true;
stk.push(*(t.right));
}
else
{
cout<<stk.top().data<<" ";
stk.pop();
}
}
}
根据一棵树的先序遍历和中序遍历,或者后序遍历和中序遍历序列,都可以唯一地确定一棵树。如果只知道先序和后序遍历,则不能唯一确定一棵树。
2.2 、按层次遍历二叉树
实现方法为从上层到下层,每层中从左侧到右侧依次访问每个结点。下面我们将给出一棵二叉树及其按层次顺序访问其中每个结点的遍历序列。
(1)顺序存储时,按层遍历的算法实现:
void LevelOreder(QBTreeBT)
{
for (i=1;i<=BT.n;i++)
if (BT.elem[i]!='#')Visite(BT.elem[i]);
}
(2)二叉树用链式存储结构表示时,按层遍历的算法实现:
在这个算法中,应使用一个队列结构完成这项操作。所谓记录访问结点就是入队操作,而取出记录的结点就是出队操作。这样一来,我们的算法就可以描述成下列形式:
访问根结点,并将根结点入队;
当队列不空时,重复下列操作:
从队列退出一个结点;
若其有左孩子,则访问左孩子,并将其左孩子入队;
若其有右孩子,则访问右孩子,并将其右孩子入队;
voidLevelOrder(BTree *BT)
{
if (!BT) exit;
InitQueue(Q); p=BT; //初始化
Visite(p); EnQueue(&Q,p); //访问根结点,并将根结点入队
while (!QueueEmpty(Q))
{
//当队非空时重复执行下列操作
DeQueue(&Q,&p); //出队
if (!p->Lchild)
{Visite(p->Lchild);EnQueue(&Q,p->Lchild);}//处理左孩子
if (!p->Rchild)
{Visite(p->Rchild);EnQueue(&Q,p->Rchild);}//处理右孩子
}
}
3:典型二叉树的操作算法
3.1、 输入一个二叉树的先序序列,构造这棵二叉树
为了保证唯一地构造出所希望的二叉树,在键入这棵树的先序序列时,需要在所有空二叉树的位置上填补一个特殊的字符,比如,'#'。在算法中,需要对每个输入的字符进行判断,如果对应的字符是'#',则在相应的位置上构造一棵空二叉树;否则,创建一个新结点。整个算法结构以先序遍历递归算法为基础,二叉树中结点之间的指针连接是通过指针参数在递归调用返回时完成。
算法:
BTreePre_Create_BT( )
{
getch(ch);
if (ch=='#') return NULL; //构造空树
else
{
BT=(BTree)malloc(sizeof(BTLinklist));//构造新结点
BT->data=ch;
BT->lchild =Pre_Create_BT( );//构造左子树
BT->rchild =Pre_Create_BT( );//构造右子树
return BT;
}
}
3.2、 计算一棵二叉树的叶子结点数目
这个操作可以使用三种遍历顺序中的任何一种,只是需要将访问操作变成判断该结点是否为叶子结点,如果是叶子结点将累加器加1即可。下面这个算法是利用中序遍历实现的。
算法:
void Leaf(BTreeBT, int *count)
{
if (BT)
{
Leaf(BT->child,&count); //计算左子树的叶子结点个数
if(BT->lchild==NULL&&BT->rchild==NULL) (*count)++;
Leaf(BT->rchild,&count); //计算右子树的叶子结点个数
}
}
3.3、 交换二叉树的左右子树
许多操作可以利用三种遍历顺序的任何一种,只是某种遍历顺序实现起来更加方便一些。而有些操作则不然,它只能使用其中的一种或两种遍历顺序。将二叉树中所有结点的左右子树进行交换这个操作就属于这类情况。该操作需要使用后续遍历
算法:
void change_left_right(BTree BT)
{
if (BT)
{
change_left_right(BT->lchild);
change_left_right(BT->rchild);
BT->lchild<->BT->rchild;
}
}
3.4 、求二叉树的高度
这个操作使用后序遍历比较符合人们求解二叉树高度的思维方式。首先分别求出左右子树的高度,在此基础上得出该棵树的高度,即左右子树较大的高度值加1。
算法:
int hight(BTreeBT)
{
//h1和h2分别是以BT为根的左右子树的高度
if (BT==NULL) return 0;
else
{
h1=hight(BT->lchild);
h2=hight(BT->right);
return max{h1,h2}+1;
}
}
六:树的存储结构及与二叉树的转换
1:树的存储结构
在计算机中,树的存储有多种方式,既可以采用顺序存储结构,也可以采用链式存储结构,但无论采用何种存储方式,都要求存储结构不但能存储各结点本身的数据信息,还要能唯一地反映树中各结点之间的逻辑关系。下面介绍几种基本的树的存储方式。
1.1、双亲表示法
用一组连续的存储空间(一维数组)存储树中的各个结点,数组中的一个元素表示树中的一个结点,数组元素为结构体类型,其中包括结点本身的信息以及结点的双亲结点在数组中的序号,树的这种存储方法称为双亲表示法。其存储表示可描述为:
数组结点结构:
typedef struct PTNode
{
TElemType data;
int parent; // 双亲位置域
} PTNode;
树结构:
typedef struct
{
PTNode nodes[MAX_TREE_SIZE];
int n; // 根结点的位置和结点个数
} PTree;
如下图:
树的双亲表示法对于实现Parent(t,x)操作和Root(x)操作很方便,但若求某结点的孩子结点,即实现Child(t,x,i)操作时,则需要查询整个数组。此外,这种存储方式不能反映各兄弟结点之间的关系,所以实现RightSibling(t,x)操作也比较困难。
1.2、孩子链表表示法
孩子链表法是将树按如下图所示的形式存储。其主体是一个与结点个数一样大小的一维数组,数组的每一个元素有两个域组成,一个域用来存放结点信息,另一个用来存放指针,该指针指向由该结点孩子组成的单链表的首位置。单链表的结构也由两个域组成,一个存放孩子结点在一维数组中的序号,另一个是指针域,指向下一个孩子。
孩子结点结构:
typedef struct CTNode
{
int child;
structCTNode *next;
}*ChildPtr;
数组结点结构:
typedef struct
{
TElemtypedata;
ChildPtrfirstchild; // 孩子链的头指针
}CTBox;
树结构:
typedef struct
{
CTBoxnodes[MAX_TREE_SIZE];
int n, r;// 结点数和根结点的位置
}CTree;
如下图:
在孩子表示法中查找双亲比较困难,查找孩子却十分方便,故适用于对孩子操作多的应用。
1.3、双亲孩子表示法
双亲孩子表示法是将双亲表示法和孩子表示法相结合的结果。其仍将各结点的孩子结点分别组成单链表,同时用一维数组顺序存储树中的各结点,数组元素除了包括结点本身的信息和该结点的孩子结点链表的头指针之外,还增设一个域,存储该结点双亲结点在数组中的序号。
如下图:
4、孩子兄弟表示法(树的二叉链表存储表示法)
这是一种常用的存储结构。其方法是这样的:在树中,每个结点除其信息域外,再增加两个分别指向该结点的第一个孩子结点和下一个兄弟结点的指针。在这种存储结构下,树中结点的存储表示可描述为:
typedefstruct CSNode
{
TElemtype data;
struct CSNode*firstchild, *nextsibling;
}CSNode, *CSTree;
如下图:
2:森林与二叉树的转换
从树的孩子兄弟表示法可以看到,二叉树与树都可用二叉链表作为存储结构,则以二叉链表作为媒介可导出树与二叉树之间的一个对应关系。也就是说给定一棵树,可以找到唯一的一棵二叉树与之对应。
如下图:
从树的二叉链表表示的定义可知,任何一棵树对应的二叉树的根节点的右子树必空。若把第二棵树的根节点看成是第一棵树的根节点的兄弟,则同样可导出森林与二叉树的对应关系。
如下图:
由树结构的定义可引出两种次序遍历树的方法,一种是先根遍历树:先访问树的根节点,然后依次先根遍历根的每棵子树;另一种是后根遍历:先依次后根遍历每棵子树,然后访问根节点。
当以二叉链表作树的存储结构时,树的先根遍历和后根遍历可借用二叉树的先序遍历和中序遍历的算法实现。
七:哈夫曼树及哈夫曼编码
1.树的路径长度
树的路径长度是从树根到树中每一结点的路径长度之和。在结点数目相同的二叉树中,完全二叉树的路径长度最短。
2.树的带权路径长度(Weighted Path Length of Tree,简记为WPL)
结点的权:在一些应用中,赋予树中结点的一个有某种意义的实数。
结点的带权路径长度:结点到树根之间的路径长度与该结点上权的乘积。
树的带权路径长度(Weighted Path Length of Tree):定义为树中所有叶结点的带权路径长度之和,通常记为:
其中:
n表示叶子结点的数目
wi和li分别表示叶结点ki的权值和根到结点ki之间的路径长度。
树的带权路径长度亦称为树的代价。
3.最优二叉树或哈夫曼树
在权为wl,w2,…,wn的n个叶子所构成的所有二叉树中,带权路径长度最小(即代价最小)的二叉树称为最优二叉树或哈夫曼树。
【例】给定4个叶子结点a,b,c和d,分别带权7,5,2和4。构造如下图所示的三棵二叉树(还有许多棵):
它们的带权路径长度分别为:
(a)WPL=7*2+5*2+2*2+4*2=36
(b)WPL=7*3+5*3+2*1+4*2=46
(c)WPL=7*1+5*2+2*3+4*3=35
其中(c)树的WPL最小,可以验证,它就是哈夫曼树。
注意:
① 叶子上的权值均相同时,完全二叉树一定是最优二叉树,否则完全二叉树不一定是最优二叉树。
② 最优二叉树中,权越大的叶子离根越近。
③最优二叉树的形态不唯一,WPL最小
4:构造最优二叉树
4.1.哈夫曼算法
哈夫曼首先给出了对于给定的叶子数目及其权值构造最优二叉树的方法,故称其为哈夫曼算法。其基本思想是:
(1)根据给定的n个权值wl,w2,…,wn构成n棵二叉树的森林F={T1,T2,…,Tn},其中每棵二叉树Ti中都只有一个权值为wi的根结点,其左右子树均空。
(2)在森林F中选出两棵根结点权值最小的树(当这样的树不止两棵树时,可以从中任选两棵),将这两棵树合并成一棵新树,为了保证新树仍是二叉树,需要增加一个新结点作为新树的根,并将所选的两棵树的根分别作为新根的左右孩子(谁左,谁右无关紧要),将这两个孩子的权值之和作为新树根的权值。
(3)对新的森林F重复(2),直到森林F中只剩下一棵树为止。这棵树便是哈夫曼树。
注意:
① 初始森林中的n棵二叉树,每棵树是一个孤立的结点,它们既是根,又是叶子
② n个叶子的哈夫曼树要经过n-1次合并,产生n-1个新结点。最终求得的哈夫曼树中共有2n-1个结点。
③ 哈夫曼树是严格的二叉树,没有度数为1的分支结点。
4.2.代码实现
HMnode*huffmantree = NULL;
ints1 = -1, s2 = -1;
m= 2 * n - 1;
huffmantree= (HMnode *)malloc(m * sizeof(HMnode));
for(i= 0; i < m; i++)
{
if(i< n)
{
huffmantree[i].weight= begin[i];//先复制叶子结点的权重
}
else
{
huffmantree[i].weight= -1;
}
huffmantree[i].parent= -1;
huffmantree[i].lchild= -1;
huffmantree[i].rchild= -1;
}
intj = 0;
for(i= n; i < m; i++)
{
selectsmall(huffmantree,i,&s1,&s2);
huffmantree[i].weight= huffmantree[s1].weight + huffmantree[s2].weight;
huffmantree[s1].parent= i;
huffmantree[s2].parent= i;
huffmantree[i].lchild= s1;
huffmantree[i].rchild= s2;
}
5:编码方案
给定的字符集C,可能存在多种编码方案。
1:等长编码方案
等长编码方案将给定字符集C中每个字符的码长定为[lg|C|],|C|表示字符集的大小。
2:变长编码方案
变长编码方案将频度高的字符编码设置短,将频度低的字符编码设置较长。变长编码可能使解码产生二义性。产生该问题的原因是某些字符的编码可能与其他字符的编码开始部分(称为前缀)相同。
比如:设E、T、W分别编码为00、01、0001,则解码时无法确定信息串0001是ET还是W。
3:前缀码方案
对字符集进行编码时,要求字符集中任一字符的编码都不是其它字符的编码的前缀,这种编码称为前缀(编)码。等长编码是前缀码
4:最优前缀码
平均码长或文件总长最小的前缀编码称为最优的前缀码。最优的前缀码对文件的压缩效果亦最佳。
6:根据最优二叉树构造哈夫曼编码
利用哈夫曼树很容易求出给定字符集及其概率(或频度)分布的最优前缀码。哈夫曼编码正是一种应用广泛且非常有效的数据压缩技术。该技术一般可将数据文件压缩掉20%至90%,其压缩效率取决于被压缩文件的特征。
6.1:具体做法
用字符ci作为叶子,频率pi做为叶子ci的权,构造一棵哈夫曼树,并将树中左分支和右分支分别标记为0和1;
将从根到叶子的路径上的标号依次相连,作为该叶子所表示字符的编码。该编码即为最优前缀码(也称哈夫曼编码)。
6.2:哈夫曼编码为最优前缀码
由哈夫曼树求得编码为最优前缀码的原因:
每个叶子字符ci的码长恰为从根到该叶子的路径长度li,平均码长(或文件总长)又是二叉树的带权路径长度WPL。而哈夫曼树是WPL最小的二叉树,因此编码的平均码长(或文件总长)亦最小。
树中没有一片叶子是另一叶子的祖先,每片叶子对应的编码就不可能是其它叶子编码的前缀。即上述编码是二进制的前缀码。
6.3:求哈夫曼编码的算法
(1)思想方法
给定字符集的哈夫曼树生成后,求哈夫曼编码的具体实现过程是:依次以叶子T[i](0≤i≤n-1)为出发点,向上回溯至根为止。上溯时走左分支则生成代码0,走右分支则生成代码1。
注意:
①由于生成的编码与要求的编码反序,将生成的代码先从后往前依次存放在一个临时向量中,并设一个指针start指示编码在该向量中的起始位置(start初始时指示向量的结束位置)。
②当某字符编码完成时,从临时向量的start处将编码复制到该字符相应的位串bits中即可。
③ 因为字符集大小为n,故变长编码的长度不会超过n,加上一个结束符'\0',bits的大小应为n。
(2)代码:
int m = 2 * size - 1;
HMnode*tree = NULL;
tree= buildhuffman(set, size);
char**code = malloc(size * sizeof(char* ));//存放n个结点的编码
inti, j, k;
intstart;
for(i= 0; i < size; i++)
{
code[i]= malloc(size);
memset(code[i],0, size);
}
char*tempcode = malloc(size);
for(i= 0; i < size; i++)
{
memset(tempcode,0, size);
start= size - 2;//从叶子结点到根,因而是从后往前赋值
k= i;
j= tree[k].parent;
while(j!= -1)
{
if(tree[j].lchild== k)
{
tempcode[start--]= '0';
}
else
{
tempcode[start--]= '1';
}
k= j;
j= tree[k].parent;
}
memcpy(code[i],&(tempcode[start+1]), size-1-start);
printf("%d->%s\n",tree[i].weight, code[i]);
}
相关文章推荐
- 【算法导论】学习笔记——第10章 基本数据结构
- 算法导论10(基本数据结构)
- 基本数据结构(算法导论)与python
- 每对顶点间的最短路径基本算法 --- 算法导论笔记
- 算法导论第10章基本数据结构10.1栈
- MIT:算法导论——7.1.基本数据结构_栈、队列、链表、有根树
- 数据结构 学习笔记(一):基本概念:什么是数据结构和算法,应用实例
- 基本数据结构(2)——算法导论(12)
- 【算法导论笔记】基本图算法
- 算法导论——第七章——基本数据结构
- 算法导论学习笔记(15)——用于不相交集合的数据结构
- 算法导论 第四部分——基本数据结构——第15章:动态规划:背包问题
- 基本数据结构(算法导论)与python
- 算法导论——lec 10 图的基本算法及应用
- 【算法导论】学习笔记——第22章 图的基本算法
- 栈与队列_第10章_基本数据结构_算法导论
- 有根树的表示_第10章_基本数据结构_算法导论
- 算法导论-5.基本数据结构
- 算法导论学习笔记-第十四章-数据结构的扩张
- 算法导论 第三部分——基本数据结构——栈、队列、链表、散列表