目录
1.数据结构的概念
数据结构指的是数据元素之间的逻辑结构、存储结构及其数据的抽象运算,即按某种逻辑关系组织起来的一组数据,再按一定的存储表示方式把它们存储在计算机的存储器中,并在这些数据上定义一个运算的集合,这就叫做一个数据结构。
1.数据:描述客观事物的数、字符以及能输入计算机中并被计算机处理的符号的集合。如:整数、字符、图形、图像、声音。
2.数据元素:数据的基本单位。如:表格中的一行、图中的一个顶点。
3.数据对象:具有相同性质的数据元素的集合,是数据的一个子集。如:大写字母数据对象就是集合{‘A’,‘B’,...,‘Z’}。
4.数据的逻辑结构:数据元素间的逻辑关系,它与数据元素的存储结构无关。是独立于计算机的。数据逻辑结构主要包括线性结构和非线性结构(树结构、图结构等)
1)线性结构的特征:数据元素之间存在着一对一的关系,且结构中仅有一个开始结点和一个终端结点,其余结点都是仅有一个直接前趋和一个直接后继。
2)非线性结构的特征:数据元素之间存在着一对多或多对多的关系,即一个结点可能有多个直接前趋和多个直接后继。
5.数据的存储(物理)结构:数据的存储结构是数据在计算机中的存储表示(映像),亦称为数据的物理结构。它包括数据元素和关系的表示。数据的存储方法主要包括:顺序存储方法、链接存储方法、索引存储方法和散列存储方法。
1)顺序存储方法:把逻辑上相邻的结点存储在物理位置上也相邻的连续存储单元里。它通常是借助于程序设计语言中的数组来描述的。该方法主要应用于线性的数据结构,但非线性的数据结构也可以通过某种线性化的方法来实现顺序存储。
2)链接存储方法:用一组不一定连续的存储单元存储逻辑上相邻的元素,元素间的逻辑关系是由附加的指针域表示的。它通常是借助于程序设计语言中的指针来描述的。
3)索引存储方法:通常在存储元素信息的同时,还建立附加的索引表。表中的索引项一般形式是:(关键字,地址)。关键字是能唯一标识一个元素的一个数据项或多个数据项的组合。
4)散列存储方法:根据数据元素的关键字直接计算出该元素的存储地址。
存储结构是数据结构不可缺少的一个方面。同一种逻辑结构,采用不同的存储方法可以得到不同的存储结构。选择何种存储结构来表示相应的逻辑结构,要视具体的应用系统要求而定,主要考虑的还是运算方便及算法的时间和空间上的要求。
6.数据的运算:即对数据元素施加的操作(行为)。数据的运算是定义在数据的逻辑结构上的,每种逻辑结构都有一个运算的集合,最常用的运算如:检索、插入、删除、更新、排序等。
2.算法描述与分析
1.一个算法就是一种解题的方法。算法是由若干条指令组成的有穷序列,其中每条指令表示一个或多个操作。算法要满足以下五个准则:
(1)输入:算法开始前必须给算法中用到的变量初始化,一个算法的输入可以包含零个或多个数据。
(2)输出:算法至少有一个或多个输出。
(3)有穷性:算法中每一条指令的执行次数都是有限的,而且每一步都在有穷时间内完成,即算法必须在执行有限步后结束。
(4)确定性:算法中每一条指令的含义都必须是明确的,无二义性。
(5)可行性:算法是可行的,即算法中描述的操作都可以通过有限次的基本运算来实现。
2.设计一个好的算法,对提高程序的执行效率是至关重要的。算法优劣的评价:
(1)正确性:即对于一切合法的输入数据,该算法经过有限时间的执行都能得到正确的结果。
(2)时间复杂性:执行算法所耗费的时间。
(3)空间复杂性:执行算法所耗费的存储空间,主要是辅助空间。
(4)可读性和可操作性:易于理解、易于编程、易于调试。
3.线性表
线性表:是最简单和最常用的一种数据结构,它是由n个数据元素(结点)a1,a2,……,an组成的有限序列。
1.数据元素的个数n为表的长度。当n为零时称为空表,非空的线性表通常记为(a1,a2,……,an)。
2.元素ai是一个抽象的符号,它可以是一个数或一个符号,还可以是一个复杂记录。如一个学生的信息。
3.线性表的逻辑特性,对于一个非空的线性表:
(1)有且仅有一个称为开始元素的a1,它没有前趋,仅有一个直接后继a2。
(2)有且仅有一个称为终端元素的an,它没有后继,仅有一个直接前趋。
(3)其余元素ai(2≤i≤n-1)称为内部元素,它们有且仅有一个直接前趋ai-1和一个直接后继ai+1。
4.基本运算
(1)InitList(L):置空表,构造一个空的线性表L
(2)ListLength(L):求表长,返回线性表L中元素个数
(3)GetNode(L,i):取表中第i个元素,若1<=i<=ListLength(L),则返回第i个元素
(4)LocateNode(L,x):按值查找,在表L中查找第一个值为x的元素,并返回该元素在表L中的位置,若表中没有元素的值为x,则返回0值
(5)InsertList(L,i,x):插入,在表L的第i元素之前插入一个值为x的新元素,表L的长度加1
(6)DeleteList(L,i):删除,删除表L的第i个元素,表L的长度减1
4.线性表的顺序存储
1.线性表的顺序存储:指的是将线性表的数据元素按其逻辑次序依次存入一组地址连续的存储单元里,用这种方法存储的线性表称为顺序表。
2.线性表顺序存储结构的特点
(1)元素在表中的相邻关系,在计算机内也存在着相邻的关系。
(2)只要知道基地址和每个元素占用的单元数,就可以求出任一元素的存储地址。因此,线性表中任意一个元素都可以随机存取,所以顺序表是一种随机存取结构。
5.顺序表上基本运算
1.若表L是SeqList类型的顺序表
(1)随机存取表中第i个结点的方法:L.data[i-1]
(2)置空表:将当前表长置为0,L.length=0
2.插入运算
线性表的插入运算是指在线性表的第i-1个元素和第i个元素之间插入一个新元素x,使长度为n的线性表变为长度为n+1的线性表。
插入算法
void InsertList (SeqList *L,int i,DataType x)
{ //在顺序表L中第i个位置之前插入一个新元素x
int j;
if(i<1 || i>L->length+1){
printf("position error");
return;
}
if(L->length>=ListSize){
printf("overflow");
return;
}
//第i个元素(包括i)之后的所有元素都后移一位,从最后一个元素开始逐一后移
for(j=L->length-1;j>=i-1;j--)
L->data[j+1]=L->data[j];
L->data[i-1]=x; //插入新元素x
L->length++; //实际表长加1
}
3.删除运算
线性表的删除运算是指将表中第i(1≤i≤n)个元素删除,使长度为n的线性表变为长度为n-1的线性表。
删除算法
DataType DeleteList(SeqList*L,int i)
{ //在顺序表L中删除第i个元素,并返回被删除元素
int j:
DataType x;//DataType 是一个通用类型标识符,在使用时再定义实际类型
if(i<1||i>L->length){
printf ("position error");
exit(0): //出错退出处理
}
x=L->data[i]; //保存被删除元素
//第i个元素之后的所有元素都前移一位
for(j=i;j<=L->length;j++)
L->data[j-1]=L->data[j];//元素前移
L->length--; //实际表长减1
return x; //返回被删除的元素
}
4.线性表顺序存储结构特点:
(1)存取元素方便:逻辑上相邻的两个元素在物理位置上也是相邻的,因此可以随机存取表中任一元素。
(2)插入和删除元素时,需要移动大量的元素。
6.线性表的链式存储结构
1.线性表链式存储:存储区域可以连续也可以不连续,包括单链表、循环链表和双向链表等存储方式。
2.线性表链式存储特点:
(1)链式存储结构的元素不可以随机存取。
(2)插入、删除元素方便,不需要移动大量的元素。
3.单链表(线性链表)
(1)线性表的链式存储结构:由n个结点链成一个链表,这种链表每个结点只包含一个指针域,因此称为单链表。
(2)单链表的结构特征:
a)单链表中每个结点的存储地址是存放在其直接前趋结点的指针域中。
b)开始结点无前趋结点,设立头指针head指向开始结点。
c)终端结点无后继结点,终端结点的指针域为空,即NULL。
d)如果链表中一个结点也没有,则为空链表,这时head=NULL。
(3)建立单链表
a)头插法建表
i)从一个空表开始,重复读入数据;生成新结点(malloc函数),将读入的数据存放到新结点的数据域中;将新结点插入到当前链表的表头上,直到读入结束标志为止。
ii)头插法建表算法
LinkList CreateListF( )
{
LinkList head;
ListNode *p;
char ch;
head = NULL; //置空单链表
ch = getchar( ); //读入第一个字符
while(ch! = ‘\n’) { //读入字符不是结束标志符时作循环
p = (ListNode *) malloc(sizeof(ListNode)); //申请新结点
p ->date = ch; //数据域赋值
p->next = head; //指针域赋值
head = p; //头指针指向新结点
ch = getchar( ); //读入下一个字符
}
return head; //返回链表的头指针
}
b)尾插法建表
i)尾插法是将新结点插入到链表的表尾上,在设计算法时,需要设一个尾指针方便操作。
ii)尾插法建表算法
LinkList CreateListR( )
{
LinkList head,rear;
ListNode *p;
char ch;
head=NULL ; rear=NULL;
ch=getchar( );
while(ch!=‘\n’){
p=(ListNode *)malloc(sizeof(ListNode));
p->data=ch;
if (head==NULL){
head=p;
}else{
rear->next=p;
}
rear=p;
ch=getchar( );
}
if(rear!=NULL) rear->next=NULL;
return head;
}
(4)按结点序号查找(带头结点)
a)算法思路:要查找第i个结点,必须从开始结点开始搜索(头结点序号为0);指针变量p指向当前结点;设置变量j为计数器,初始值为1,p扫描下一个结点时,计数器加1;当j=i时,指针p所指向的结点就是要找的结点。
b)算法实现:
listNode *GetNodeByI(LinkList head,int i){
ListNode *p;
int j;
p=head->next;
j=1;
while(p != NULL && j<i){
p=p->next;
++j;
}
if(j==i){
return p;
}else{
return NULL;
}
}
(5)按结点值查找(带头结点)
a)算法思路:从链表的开始结点出发,顺链逐个将结点的值和给定值k进行比较,若遇到相等的值,则返回该结点的存储位置,否则返回NULL。
b)算法实现:
listNode *GetNodeByK(LinkList head,DataType k){
ListNode *p = head->next;
while(p && p->data != k){
p=p->next;
}
return p;
}
(6)插入运算
a)插入运算:是将值为x的新结点插入到表的第i个结点位置上。
b)算法思路:先利用一个指针变量(如p)指向ai-1的位置;生成新结点(如*s),让新结点的指针域指向p结点指针域指向的结点,即s->next=p->next;指针p的指针域指向结点s,即p->next=s。
c)算法实现:
void InsertList(LinkList head,int i,DataType x){
ListNode *p,*s;
int j;
p = head;
j = 0;
while(p != NULL && j<i-1){
p = p->next;
++j;
}
if(p == NULL){
printf("异常");
return ;
}else{
s = (ListNode *)malloc(sizeof(ListNode));
s->data = x;
s->next = p->next;
p->next = s;
}
}
(7)删除运算
a)删除运算:将链表中第i个结点从表中删去。
b)算法思路:先定义一个指针变量(如p)指向要删除的结点(如i结点)的前一个结点i-1;让指针p的指针域指向要删除的结点的下一个结点i+1;将第i个结点释放掉。
c)算法实现:
DataType DeleteList(LinkList head,int j){
ListNode *p,*s;
DataType x;
int j;
p = heade;
j = 0;
while(p!=NULL && j<i-1){
p = p->next;
++j;
}
if(p == NULL){
printf("异常");
exit(0);
}else{
s = p->next;
p->next = s->next;
x = s->data;
free(s);
return x;
}
}
4.循环链表
(1)循环链表是单链表最后一个结点(终端结点)的指针域指向链表的头结点,使整个链表构成一个环。
(2)和单链表的区别:
a)从表中任一结点都可以访问表中其他结点。
b)算法中循环结束判断条件不再是p或p->next是否为空,而是它们是否等于头指针。
5.双向链表
在单链表的结点类型中增加一个指向其直接前趋的指针域prior。这样形成的链表中有两条不同方向的链,因此称为双向链表。
7.顺序表和链表的比较
(1)顺序存储结构:可以随机存取表中任一元素,但插入和删除元素时,需要移动大量的元素。
(2)链式存储结构:插入和删除元素不需要移动大量的元素,但不可以随机存取表中的元素。
1.时间性能
顺序存储结构每个结点存取的时间复杂度是O(1);
链式存储结构必须从表头开始沿链逐一访问各结点,存取元素的时间复杂度为O(n)。
如果对线性表经常性的查找运算,顺序表形式存储为宜。
如果需要经常运算的是插入和删除操作,以链式存储结构为宜。
2.空间性能
顺序存储结构:存储空间是静态分配的,在应用程序执行之前必须给定空间大小。
适用情况:预先能够确定存储空间大小,适合使用该存储结构。
链式存储结构:动态分配存储空间。
适用情况:数据量变化较大的动态问题,适合链式存储结构。
3.存储密度
存储密度:结点空间的利用率,存储密度越大,则存储空间利用率越高。
存储密度=(结点数据域所占空间)/(整个结点所占空间)
顺序表结点的存储密度是1;如果不考虑顺序表的空闲区,则它的存储空间利用率为100%。
链表结点的存储密度小于1。
8.栈的定义及其运算
1.栈是限定在表的一端进行插入和删除运算的线性表。将插入、删除的一端称为栈顶(top),另一端称为栈底。遵循的原则是后进先出,也称为LIFO表。
2.栈的运算
(1)InitStack(&S):置空栈,构造一个空栈S
(1)StackEmpty(S):判栈空,若栈S为空栈,则返回TRUE,否则返回FALSE
(1)StackFull(S):判栈满,若栈S为满栈,则返回TRUE,否则返回FALSE
(1)Push(&S,x):进栈,将元素x插入S栈的栈顶
(1)Pop(&S):退栈,若栈S为非空,则将S的栈顶元素删除,并返回栈顶元素
(1)GetTop(S):取栈顶元素,若S栈为非空,则返回栈顶元素,但不改变栈的状态
9.栈的存储表示和实现
1.栈的顺序存储结构——顺序栈
(1)栈受限的线性表,因此顺序栈也是用数组实现
a)将栈底位置设置在数组的最低端(下标为0);
b)栈顶位置随进栈和退栈而变化,使用一个整型量top来指示当前栈顶位置,top通常称为栈顶指针。
c)S.data[0]是栈底元素
d)栈顶S.data[top]正向增长,即进栈时S.top加1,退栈时S.top减1。
e)S.top<0表示空栈,S.top=StackSize-1表示栈满。
f)当栈满时再做进栈运算会产生空间溢出,称为“上溢”。
g)当栈空时退栈也将产生溢出,简称“下溢”。
(2)顺序栈基本运算的实现
a)置空栈
void InitStack(SeqStack *S){
s->top=-1;
}
b)判断栈是否为空
void StackEmpty(SeqStack *S){
return S->top==-1;
}
c)判断栈是否已满
void StackFull(SeqStack *S){
return S->top == StackSize-1;
}
d)进栈
void Push(SeqStack *S,DataType x){
if(StackFull(S)){
printf("栈满");
return;
}
S->top = S->top+1;
S->data[S->top]=x;
}
e)退栈
DataType Pop(SeqStack *S){
if(StackEmpty(S)){
printf("栈空");
exit(0);
}
return S->data[S->top--];
}
f)取栈顶(不改变栈顶指针)
DataType GetTop(SeqStack *S){
if(StackEmpty(S)){
printf("栈空");
exit(0);
}
return S->data[S->top];
}
2.栈的链式存储结构及基本操作
(1)栈的链式存储结构称为链栈。克服了顺序存储分配固定空间所产生的溢出和空间浪费问题(顺序栈需要预先分配存储空间,如果空间不足会出现溢出,如果空间太大会造成浪费)。
链栈的插入和删除操作仅限制在表头(栈顶)进行,所以不用设置头结点,将单链表的头指针head改为top即可。
(2)链栈的基本运算
a)判断栈是否为空
int StackEmpty(LinkStack top){
return top == NULL;
}
b)进栈
LinkStack Push(LinkStack top,DataType x){
StackNode *p;
p = (StackNode)malloc(sizeof(StackNode));
p->data = x;
p->next = top;
top = p;
return top;
}
c)退栈
ListStack Pop(LinkStack top,DataType *x){
StackNode *p = top;
if(StackEmpty(top)){
printf("栈空");
exit(0);
}
*x = p->data;
top = p->next;
free(p);
return top;
}
d)取栈顶
DataType GetTop(LinkStack top){
if(StackEmpty(top)){
printf("栈空");
exit(0);
}
return top->data;
}
10.队列的定义及其运算
1.队列也是一种操作受限的线性表,它只允许在表的一端进行元素的插入(队尾rear),而在另一端进行元素的删除(队头front)。
(1)在队列中,把元素的插入称为入队,元素的删除称为出队
(2)入队的一端称为队尾,出队的一端称为队头
(3)队列中排在最前面的总是最先离开队列,新成员总加入队尾,即队列的操作原则:先进先出(FIFO),队列也称为先进先出(FIFO)表
2.队列的运算
(1)InitQueue(Q):置空队列,构造一个空队列Q
(2)QueueEmpty(Q):判队空,若栈Q为空队列,则返回TRUE,否则返回FALSE
(3)EnQueue(Q,x):入队列,若队列不满,则将数据x插入到Q的队尾
(4)DeQueue(Q):出队列:若队列不为空,则删除队头元素,并返回该元素
(5)GetFront(Q):取队头,若队列不空,则返回队头元素
11.队列的存储表示和实现
1.顺序队列
队列的顺序存储结构称为顺序队列。顺序存储结构也是利用一块连续的存储单元存放队列中的元素;队列的队头和队尾位置是变化的,需要设置两个指针front和rear分别指示队头和队尾
(1)入队运算:将新元素插入到rear所指的位置,再将rear加1
Q.data[Q.rear]=x;
Q.rear=Q.rear+1;
(2)出队运算:取出出队的元素,将front加1,并返回被删除的元素
x=Q.data[Q.front];
Q.front=Q.front+1;
return x
(3)判断顺序队列为空:当头尾指针相等时,即Q.front==Q.rear,队列为空
2.链队列
队列的链式存储结构称为链队列。链队列是限制在表头删除和表尾插入的单链表,为方便表尾插入元素,需要在单链表中再增加一个尾指针。
带头结点链队列的基本运算
(1)构造空队列
void InitQueue( LinkQueue *Q ){
Q->front=(QueueNode *)malloc( sizeof(QueueNode));
Q->rear=Q->front;
Q->rear->next=NULL;
}
(2)判队空
int QueueEmpty ( LinkQueue *Q){
return Q->rear==Q->front;
}
(3)入队列
void EnQueue (LinkQueue *Q ,DataType x){
QueueNode *p=(QueueNode *)malloc(sizeof(QueueNode));
p->data=x;
p->next=NULL;
Q->rear->next=p;
Q->rear=p;
}
(4)取队头元素
DataType GetFront (LinkQueue *Q ){
if (QueueEmpty(Q)) {
printf(“Queue underflow”);
exit(0);
}
return Q->front->next->data;
}
(5)出队列
情况一:当队列的长度大于1时,则出队操作只需要修改头结点的指针域即可,尾指针不变
s=Q->front->next;
Q->front->next=s->next;
x=s->data;
free(s); return x;
情况二:当队列的长度等于1时,出队时不仅要修改头结点指针域,而且还需要修改尾指针
s=Q->front->next;
Q->front->next=NULL;
Q->rear=Q->front
x=s->data;
free(s); return x;
12.中缀表达式到后缀表达式的转换
1.中缀表达式:人工计算的表达式采用的是中缀形式,也就是运算符放在操作数中间
2.后中缀表达式:中缀表达式不适合计算机处理,需要使用后缀表达式;即将运算符放在操作数之后
3.中缀表达式转换为后缀表达式通过栈完成,算法思想:
(1)顺序扫描中缀算术表达式,读到数字时,直接将其送至输出队列中;
(2)当读到运算符时,将栈中所有优先级高于或等于该运算符的运算符弹出,送至输出队列,再将当前运算符入栈;
(3)当读到左括号时,入栈;读到右括号时,将靠近栈顶的第一个左括号上面的运算符全部依次弹出,送至输出队列中,再删除栈中的左括号。
13.矩阵的压缩存储
1.特殊矩阵——对称矩阵
对称矩阵中的元素是关于主对角线对称的,只需存储矩阵上三角或下三角元素即可,让两个对称元素共享一个存储空间。这样能够节省近一半的存储空间。
按C语言的“按行优先”存储主对角线(包括主对角线)以下的元素,总数为:n(n+1)/2个
2.特殊矩阵——三角矩阵
以主对角线划分,三角矩阵有上三角和下三角两种。
下三角矩阵:主对角线上方均为常数c或0的n阶矩阵。
上三角矩阵:主对角线下方均为常数c或0的n阶矩阵。
三角矩阵中重复元素c可共享一个存储空间,其余元素有n(n+1)/2个,三角矩阵可压缩存储在一维数组sa[n(n+1)/2+1]中,c存放在数组的最后一个元素中。
3.稀疏矩阵
矩阵中含有s个非零元素,s远远小于矩阵元素的总数,这种矩阵称为稀疏矩阵。稀疏矩阵非零元素的分布一般没有规律。由于非零元素分布没有规律,因此存储的时候除存储元素外,还必须存储该元素的行、列位置(即下标)可用一个三元组(i,j, aij)来唯一确定一个非零元素。
三元组表示非零元素时,对稀疏矩阵通常有两种方法:顺序存储和链式存储
14.树的基本概念
1.树形结构是一类重要的非线性数据结构,树是n(n≥0)个结点的有限集T。它或是空集(空树即n=0),或者是非空集。对于任意一棵非空树:
(1)有且仅有一个特定的称为根(Root)的结点;
(2)当n>1时,其余的结点可分为m(m>0)个互不相交的有限集T1,T2,…,Tm,其中每个集合本身又是一棵树,并称为根的子树。
2.树的表示法
(1)树形图表示法
(2)嵌套集合表示
(3)凹形表表示
(4)广义表表示
3.森林:是m棵互不相交的树的集合。
若将一棵树的根结点删除,就得到该树的子树所构成的森林;将森林中所有树作为子树,用一个根结点把子树都连起来,森林就变成一棵树。
15.二叉树的定义及性质
1.二叉树:二叉树是n个结点的有限集合,它的每个结点至多只有两棵子树。
2.满二叉树:每一层上结点数都达到最大值,因此不存在度数为1的结点;所有叶子结点都在第k层上。
2.完全二叉树:若一棵深度为k的二叉树,其前k-1层是一棵满二叉树,而最下面一层(即第k层)上的结点都集中在该层最左边的若干位置上,则称此二叉树为完全二叉树。
16.二叉树的存储结构
1.顺序存储结构
1)顺序存储有n个结点的完全二叉树方法:
步骤一:从树根开始自上到下,每层从左到右给结点编号(假定编号从0开始),得到一个反映整个二叉树结构的线性序列。
步骤二:以各结点的编号为下标,把每个结点的值对应存储到一个一维数组中。
2)顺序结构存储完全二叉树的特点
由一个结点的编号,可以推得其双亲结点及左、右孩子等结点的编号。
完全二叉树除最下面一层外,各层结点数都达到最大值,每一层上结点个数恰好是上一层结点个数的2倍。
3)顺序存储二叉树的优缺点
a)对于完全二叉树,使用顺序存储结构既简单又节省存储空间
b)对于一般二叉树,有可能造成存储空间浪费(为了使用结点在数组中的相对位置来表示结点之间的逻辑关系,就必须增加一些虚结点,使其成为完全二叉树形式。)
2.链式存储结构(二叉链表)
对于一般二叉树,链式存储结构比顺序存储节省空间,且在插入或删除结点时,无需移动大量结点。
1)二叉树链式存储结构的表示方法:每个结点设置值域、左指针域和右指针域
a)值域(data):存放结点的值
b)左子树(左孩子)指针(lchild):指向左子树(左孩子)
c)右子树(右孩子)指针(rchild):指向右子树(右孩子)
2)在一棵二叉树中,设有一个指向其根结点(即开始结点)的BinTree型的头指针bt及所有类型为BinTNode的结点,就构成了二叉树的链式存储结构,称其为二叉链表。
17.二叉树的遍历
遍历:沿着某条搜索路径(线)周游二叉树,依次对树中每个结点访问且仅访问一次。遍历对于线性结构只需从头到尾访问即可;二叉树由于每个结点都有两个后继结点,这将导致存在多条遍历路径。
1.递归遍历算法
根据二叉树的递归定义,遍历一棵非空二叉树问题可分解为三个子问题:访问根结点(D)、遍历左子树(L)和遍历右子树(R)。
因此遍历的方案有:
DLR、LDR、LRD(先左后右)
DRL、RDL、RLD(先右后左)
a)DLR:遍历的次序是根结点、左子树、右子树,由于根结点操作在遍历左、右子树之前,因此又称前序遍历或先根遍历。
前序遍历二叉树的递归定义
若二叉树非空,则依次进行操作:
(1)访问根结点;(2)前序遍历左子树;(3)前序遍历右子树。
算法实现:
void Preorder(BinTree bt){
if(bt != NULL){
printf("%c",bt->data);
Preorder(bt->lchild);
Preorder(bt->rchlid);
}
}
b)LDR:遍历的次序是左子树、根结点、右子树,由于根结点操作在遍历左子树之后、遍历右子树之前,因此又称中序遍历或中根遍历。
中序遍历二叉树的递归定义
若二叉树非空,则依次进行操作:
(1)中序遍历左子树;(2)访问根结点;(3)中序遍历右子树。
算法实现:
void Inorder(BinTree bt){
if(bt != NULL){
Inorder(bt->lchild);
printf('%c',bt->data);
Inorder(bt->rchild);
}
}
c)LRD:遍历的次序是左子树、右子树、根结点,由于根结点操作在遍历左子树、右子树之后,因此又称后序遍历或后根遍历。
后序遍历二叉树的递归定义
若二叉树非空,则依次进行操作:
(1)后序遍历左子树;(2)后序遍历右子树;(3)访问根结点。
算法实现:
void Postorder(BinTree bt){
if(bt != NULL){
Postorder(bt->lchild);
Postorder(bt->rchild);
printf("%c",bt->data);
}
}
18.树的存储结构
1.双亲表示法
在树的结构中,每个结点的双亲是唯一的。
如果以一组连续的空间(如:数组)来存储树的结点,同时为每个结点附设一个指向双亲的指针parent,就可以唯一表示一棵树。
2.孩子链表法
由于树中每个结点可能有多棵子树,因此可以把每个结点的孩子结点看成一个线性表,并以单链表结构存储其孩子结点,这样,n个结点就有n个孩子链表。
为方便查找,可将树中各结点的孩子链表的头结点存放在一个指针数组中。
3.孩子兄弟表示法
孩子兄弟表示法又称二叉链表表示法:以二叉链表作为树的存储结构,链表中两个链指针域分别指向该结点的第一个孩子结点(firstchild)域和下一个兄弟结点(nextsibling)域。
19.树、森林和二叉树的转换
1.将一棵树转换成二叉树的方法
(1)所有兄弟结点之间加一道连线。
(2)每个结点保留长子连线,去掉该结点与其他孩子的连线。
(3)将所有兄弟之间的连线按顺时针方向旋转45度更清楚。
由于树根没有兄弟,所以转换后的二叉树,根结点的右子树必为空
2.将森林转换成二叉树的方法
(1)先将森林中的每棵树转化为二叉树。
(2)将各二叉树的根结点看作是兄弟连在一起,形成一棵二叉树。
20.哈夫曼树
1.哈夫曼树又称最优树,是一类带权路径长度最短的树。
路径:若在一棵树中存在着一个结点序列k1,k2….kj使得ki是ki+1的父结点(1≤i≤j),则称该结点序列是从k1到kj的一条路径。
权:将树中的结点赋上一个具有某种意义的实数,称此实数为该结点的权。
树的带权路径长度(WPL):树中所有叶子结点的带权路径长度之和称为树的带权路径长度。
2.哈夫曼算法
3.哈夫曼编码
21.图的定义和基本术语
1.图是一种复杂的非线性结构
线性结构:数据元素之间满足唯一的线性关系,每个数据元素只有一个直接前趋和一个直接后继。
树形结构:数据元素之间有着明显的层次关系,并且每个元素只与上一层中一个元素(双亲结点)及下一层中多个元素(孩子结点)相关。
图形结构:结点之间的关系可以是任意的,图中任意两个元素之间都可能相关。
2.图G由两个集合V和E组成,定义为G=(V,E)
V:是顶点的有限非空集合
E:是由V中顶点偶对表示的边的集合
V(G)和E(G)分别表示图G的顶点集合和边集合。E(G)可以为空集,即图G只有顶点而没有边。
3.有向图:对于一个图G,若每条边都是有方向的,则称该图为有向图。在有向图中,一条有向边是由两个顶点组成的有序对,通常用尖括号表示。 如:<vi,vj>表示一条有向边: vi是起点, vj是终点; 此边称为顶点vi的出边,顶点vj的入边。有向边又称为弧,边的起点称为弧尾,边的终点称为弧头。
4.无向图:对于一个图G,若每条边都是没有方向的,则称该图为无向图。在无向图中,边均是顶点的无序对,通常用圆括号表示。因此,(vi,vj )和(vj ,vi)表示同一条边。
5.顶点的度:
有无向图中:顶点v的度是以该顶点为端点的边的数目,记为D(v)。
在有向图中:顶点v的度分为入度ID(v)和出度OD(v):
1)入度:是以该顶点为终点的入边数目。
2)出度:是以该顶点为起点的出边数目。
3)该顶点的度等于其入度和出度之和。
22.插入排序
插入排序的基本思想:每次将一个待排序的记录按其关键字的大小插入到前面已排好序的文件中的适当位置,直到全部记录插入完为止。
1.直接插入排序
算法思想:每一次将一个待排序的关键字按照其值的大小插入到已经排好的部分有序序列的适当位置上,直到所有待排序关键字都被插入到有序序列中为止。
基本操作:
(1)假设待排序记录存储在数组R[1..n]中;
(2)在排序过程的某一时刻,R被划分为两个子区间,R[1..i-1]和R[i..n],前一个为已排好序的有序区,后一个为无序区。
2.希尔排序
希尔排序又称“缩小增量排序”,它是由希尔提出。它的本质还是插入排序,只是将待排序列按某种规则分成几个子序列。
算法思想:
(1)先取定一个小于n的整数d1作为第一个增量,把数组R中全部元素分成d1个组,所有下标距离为d1的倍数的元素放在同一组中,即
R[1],R[1+d1],R[1+2d1]….为第一组,
R[2],R[2+d1],R[2+2d1]….为第二组,
……….
接着在各组内进行直接插入排序。
(2)然后再取d2(d2〈d1)为第二个增量,重复上述分组和排序,直到所取的增量为d1=1,把所有的元素放在同一组中进行直接插入排序为止。
23.交换排序
交换排序的基本思想:两两比较待排序记录的关键字,如果发现两个记录的次序相反时即进行交换,直到所有记录都没有反序时为止。
1.冒泡排序
冒泡排序基本思想:通过相邻元素之间的比较和交换,使关键字较小的元素逐渐从底部移向顶部,就像水底下气泡一样逐渐向上冒泡,所以使用该方法的排序称为“冒泡”排序;冒泡排序又称为“起泡”排序。
冒泡排序过程:
(1)首先将R[n].key和R[n-1].key进行比较,若R[n].key<R[n-1].key,则R[n]和R[n-1]交换,使轻者上浮,重者下沉,接着比较R[n-1].key和R[n-2].key,同样轻者上浮,重者下沉,依此类推,直到比较R[2].key和R[1].key,若反序则交换,第一趟排序结束,此时,记录R[1]的关键字最小。
(2)然后再对R[n]-R[2]的记录进行第二趟排序,使次小关键字的元素被上浮到R[2]中,重复进行n-1趟后,整个冒泡排序结束。
2.快速排序
快速排序基本思想:首先在当前无序区R[low..high]中任取一个记录(设x)作为排序比较的基准,用此基准将当前无序区划分为两个较小的无序区R[low..i-1]和R[i+1..high],并使左边的无序区中所有记录的关键字均小于等于基准的关键字,而基准记录x则位于最终排序的位置i上
即R[low..i-1]中关键字<=x.key<=R[i+1..high]中的关键字。这个过程称为一趟快速排序。
当R[low..i-1]和R[i+1..high]均非空时,分别对它们进行上述划分,直到所有的无序区中的记录均已排好序为止。
快速排序的具体操作:
设两个指针i和j,它们的初值分别为low和high,基准记录x=R[i]
首先从j所指位置起向前搜索找到第一个关键字小于基准x.key的记录存入当前i所指向的位置,i自增1,然后再从i所指位置起向后搜索,找到第一个关键字大于x.key的记录存入当前j所指向的位置上,j自减1;重复这两步,直至i等于j为止。
24.选择排序
选择排序的基本思想:每一趟在待排序的记录中选出关键字最小的记录,依次存放在已排好序的记录序列的最后,直到全部记录排序完为止。
1.直接选择排序
基本思想:每次从待排序的无序区中选择出关键字值最小的记录,将该记录与该区中第一个记录交换位置。
操作步骤:初始时,R[1..n]为无序区,有序区为空。
(1)第一趟排序是在无序区R[1..n]中选出最小记录,将它与R[1]交换,R[1]为有序区。
(2)第二趟排序是在无序区R[2..n]中选出最小记录与R[2]交换,此时R[1..2]为有序区。
2.堆排序
堆排序是一种树形选择排序,它的基本思想:在排序过程中,将记录数组R[1..n]看成是一棵完全二叉树的顺序存储结构,利用完全二叉树中双亲结点和孩子结点之间的内在关系,在当前无序区中选择关键字最大(或最小)的记录。
堆定义:n个记录的关键字序列k1,k2,……,kn称为堆,当且仅当满足以下关系:
Ki≤k2i且Ki≤k2i+1或ki≥k2i且ki≥k2i+1()
前者称为小根堆,后者称为大根堆。
堆排序正是利用大根堆(或小根堆)来选取当前无序区中关键字最大(或最小)的记录实现排序的。
每一趟排序的操作:
将当前无序区调整为一个大根堆,选取关键字最大的堆顶记录,将它和无序区中最后一个记录交换,这正好与选择排序相反。堆排序就是一个不断建堆的过程。
堆排序的操作:
(1)把待排序文件的关键字存放在数组R[1..n]之中,将R看作一棵完全二叉树的存储结构,每个结点表示一个记录。
(2)源文件的第一个记录R[1]作为二叉树的根,以下各记录R[2..n]依次逐层从左到右顺序排列,构成一棵完全二叉树,任意结点R[i]的左孩子是R[2i],右孩子是R[2i+1],双亲是R[i/2]。
25.归并排序
归并排序的基本思想:首先将待排序文件看成n个长度为1的有序子文件,把这些子文件两两归并,得到[n/2]个长度为2的有序子文件,然后再把这[n/2]个有序的子文件两两归并,如此反复,直到最后得到一个长度为n的有序文件为止,这种方法称为二路归并排序。
26.分配排序
分配排序算法是基于不需要比较的排序算法,能使时间复杂度降为O(n)。
1.箱排序
箱排序又称桶排序,其基本思想是:
设置若干个箱子,依次扫描待排序的记录R[0],R[1],…,R[n-1],把关键字等于k的记录全部都装入第k个箱子里(分配),然后按序号依次将各非空的箱子首尾连接起来。
2.基数排序
箱排序只适用于关键字取值范围较小的情况,否则所需要箱子的数目m太多,会导致存储空间的浪费和计算时间的增长。
基数排序是对箱排序的改进和推广。
例:被排序的记录关键字ki(36,25,48,10,32,25,6,58,56,82)
分析:ki的取值是在0..99之间的整数,是两位数,可将其分解:
先对ki的个位数(ki%10)进行箱排序;
再对ki的十位数(ki/10)进行箱排序,这样只需要标号为0,1,…,9的这10个箱子进行二趟箱排序即可完成排序操作,而不需要100个箱子来进行一趟箱排序。
第一趟排序:对输入的记录序列顺序扫描,将它们按关键字的个位数字装箱,然后依箱号递增将各个非空箱子首尾连接起来,即可得到第一趟排序结果,显然结果已按个位有序。
第二趟排序:在前一趟排序结果的基础上进行,即顺序扫描第一趟的结果,将扫描到的记录按关键字的十位数字装箱,再将非空箱子首尾连接,可得到最终的排序结果。
27.算法性能分析
28.查找的基本概念
查找又称检索,是数据处理中经常使用的一种重要运算。查找运算的主要操作是关键字的比较。查找运算的主要操作是关键字的比较,通常把查找过程中的平均比较次数(也称为平均查找长度)作为衡量一个查找算法效率优劣的标准。
29.顺序表的查找
1.顺序查找
顺序查找又称线性查找,是一种最简单最基本的查找方法。
基本思想:从表的一端开始,顺序扫描线性表,依次把扫描到的记录关键字与给定的值k相比较,若某个记录的关键字等于k,则表明查找成功,返回该记录所在的下标;直到所有记录都比较完,仍未找到关键字与k相等的记录,则表明查找失败,返回0值。
顺序查找优点:简单且对表的结构无任何要求,无论是顺序存储还是链式存储,无论是否有序,都同样适用。
顺序查找缺点:效率低。
2.二分查找
二分查找又称折半查找,是一种效率较高的查找方法。
二分查找要求查找对象的线性表必须是顺序存储结构的有序表。
二分查找过程:
(1)首先将待查的k值和有序表R[1..n]的中间位置mid上的记录的关键字进行比较,若相等,则查找成功,返回该记录的下标mid。
(2)若R[mid].key〉k,则k在左子表R[1..mid-1]中,接着再在左子表中进行二分查找即可。
(3)若R[mid].key<k,则k在右子表R[mid+1..n]中,接着只要在右子表中进行二分查找即可。
这种查找方式,经过一次关键字的比较,就可以缩小一半的查找空间,如此进行下去,直到找到关键字k的记录或者当前查找区间为空时为止。
3.索引顺序查找
索引顺序查找又称分块查找,是一种介于顺序查找和二分查找之间的查找方法。
要按以下的索引方式来存储线性表:
(1)将表R[1..n]均分为b块,前B1块中的结点个数为s=|n/b| ,第b块的结点数≤s;
(2)每一块中的关键字不一定有序,但前一块中的最大关键字必须小于后一块的最小关键字,即要求表是“分块有序”的;
(3)抽取各块中的最大关键字及其起始位置构成一个索引表ID[1..b],即ID[i](1≤i≤b)中存放着第i块的最大关键字及该块在表R中的起始位置,显然,索引表是按关键字递增有序的。
分块查找的基本思想:首先查找索引表,可用二分查找或顺序查找,然后在确定的块中进行顺序查找。
4.三种查找方法的比较:
顺序查找
优点:算法简单,且对表的存储结构无任何要求,无论是顺序结构还是链式结构,也无论结点关键字是有序还是无序,都适应顺序查找。
缺点:查找效率低;当n较大时,其查找成功的平均查找长度约为表长的一半(n+1)/2,查找失败则需要比较n+1次。
二分查找
优点:速度快,效率高
缺点:要求表以顺序存储表示并且是按关键字有。另外对表结点进行插入或删除时,需要移动大量的元素。
二分查找适用于表不易变动且又经常查找的情况。
分块查找
优点:在表中插入或删除一个记录时,只要找到该记录所属的块,就可以在该块内进行插入或删除运算。因为块内记录是无序的,所以插入或删除比较容易,无需移动大量记录。
缺点:需要增加一个辅助数组的存储空间和将初始表块排序的运算,也不适宜用链式存储结构。若以二分查找确定块,则分块查找成功的平均查找长度为:log2(n/s+1)+s/2。
30.树表的查找
树表查找是对树形存储结构所做的查找。
树形存储结构和树形逻辑结构是完全对应的,都表示一个树形图,只是用存储结构中的链指针代替逻辑结构中的抽象指针,因此往往把树形存储结构(即树表)和树形逻辑结构(树)统称为树结构或树。
1.二叉排序树
二叉排序树(BST),又称二叉查找树,是一种特殊的二叉树,它或者是一棵空树,或者是具有下列性质的二叉树:
(1)若它的右子树非空,则右子树上所有结点的值均大于根结点的值。
(2)若它的左子树非空,则左子树上所有结点的值均小于根结点的值。
(3)左、右子树本身又各是一棵二叉排序树。
二叉排序树的另一个重要性质:按中序遍历二叉排序树所得到的遍历序列是一个递增有序序列。
2.B树
B树是一种平衡的多路查找树,它在文件系统中非常有用。
3.B+树
B+树是一种常用于文件组织的B树的变形树。
31.散列表查找
1.散列表的概念
散列表查找:是通过记录的关键字值进行某种运算直接求出记录的地址,是一种由关键字到地址的直接转换方法,不需要反复比较。
散列(Hash):同顺序、链式和索引存储结构一样,是存储线性表的又一种方法。
散列存储的基本思想:以线性表中的每个元素的关键字key为自变量,通过一种函数H(key)计算出函数值,把这个函数值解释为一块连续存储空间的单元地址(即下标),将该元素存储到这个单元中。
散列存储中使用的函数H(key)称为散列函数或哈希函数,它实现关键字到存储地址的映射(或称转换)。
H(key)的值称为散列地址或哈希地址,使用的数组空间是线性表进行散列存储的地址空间,所以称之为散列表或哈希表。
2.散列函数的构造方法
构造散列函数的目标是使散列地址尽可能均匀地分布在散列空间上,同时使计算尽可能简单。
(1).直接地址法
以关键字key本身或关键字加上某个常量C作为散列地址的方法。
散列函数H(key)为:H(key)=key+C
在使用时,为了使散列地址与存储空间吻合,可以调整C。
直接地址法特点:计算简单且没有冲突
适用情况:适合于关键字分布基本连续的情况,若关键字分布不连续,空号较多,将会造成较大的空间浪费。
(2)数字分析法
假设有一组关键字,每个关键字由n位数字组成,如k1,k2,…kn。数字分析法是从中提取数字分布比较均匀的若干位作为散列地址。
(3)除余数法
除余数法是选择一个适当的p(p≤散列表长m)去除关键字k,所得余数作为散列地址的方法。
对应的散列函数H(k)为:H(k)=k%p
P最好选取小于或等于表长m的最大素数。
如表长为20,那么p选19;
若表长为25,那么p选23。
(4)平方取中法
取关键字平方的中间几位作为散列地址的方法,因为一个乘积的中间几位和乘数的每一位都相关,故由此产生的散列地址较均匀,具体取多少位视实际情况而定。
(5)折叠法
首先把关键字分割成位数相同的几段(最后一段的位数可少一些),段的位数取决于散列地址的位数,由实际情况而定,然后将它们的叠加和(舍去最高进位)作为散列地址的方法。
3.散列存储冲突
如果插入的关键字计算出的散列地址所对应的存储单元已被其他元素占用,这种情况称为冲突。
具有相同散列地址的关键字称为同义词。
常用解决冲突的方法有:
(1)开放定址法
开放定址法解决冲突的基本思想:使用某种方法在散列表中形成一个探查序列,沿着此序列逐个单元进行查找,直到找到一个空闲的单元时将新结点存入其中。开放定址法主要有线性探查法、二次探查法和双重散列法。
1)线性探查法
基本思想:将散列表T[0..m-1]看成一个循环向量,若初始探查的地址为d(即H(key)=d),则后续探查地址的序列为:d+1,d+2,…,m-1,0,1,…,d-1。
插入关键字运算:若当前探查单元为空,则将关键字key写入空单元,若不空则继续后序地址探查,直到遇到空单元插入关键字,若探查到T[d-1]时仍未发现空单元,则插入失败(表满)。
2)二次探查法
二次探查法的探查序列是:
hi=(H(key)±i2)%m (0≤i≤m-1)
即探查序列为:d=H(key),d+12,d-12,d+22,d-22,…等等。
探查从地址d开始,先探查T[d],然后再依次探查
T[d+12],T[d-12],T[d+22],T[d-22],…..。
3)重散列法
双重散列法是几种方法中最好的方法,它的探查序列为:
hi=(H(key)+i*H1(key))%m (0≤i≤m-1)
即探查序列为:
d=H(key),(d+1*H1(key))%m),(d+2*H1(key))%m),….等。
(2)拉链法(链地址法)
当存储结构是链表时,多采用拉链法。
拉链法处理冲突的办法是:把具有相同散列地址的关键字(同义词)值放在同一个单链表中,称为同义词链表。
有m个散列地址就有m个链表,同时用指针数组T[0..m-1]存放各链表的头指针,凡是散列地址为i的记录都以结点方式插入到以T[i]为指针的单链表中。T中各分量的初值应为空指针
4.散列表的查找
散列表查找过程与建表过程基本一致
查找过程:给定一个关键字值K,根据建表时设定的散列函数求得散列地址,若表中该地址对应的空间是空的,则说明查找不成功;否则,将该地址单元的关键字值与K比较。若相等表明查找成功,否则再根据建表时解决冲突的方法寻找下一个地址,反复进行,直到查找成功或找到某个存储单元为空(查找不成功)为止。