数据结构 C语言版|第2版-复习

数据结构 C语言版|第2版-复习

第1章 绪论

1.2基本概念和术语

1.2.1数据、数据元素、数据项和数据对象

数据是客观事物的符号表示,是所有能输入到计算机中并被计算机程序处理的符号的总称。

数据元素是数据的基本单位。数据元素通常用于完整地描述一个对象。例如一名学生记录。

数据项是组成数据元素的、有独立含义的、不可分割的最小单位。例如学生基本信息表中的学号、姓名、性别等。

数据对象是性质相同的数据元素的集合,是数据的一个子集。

1.2.2数据结构

数据结构是相互之间存在一种或多种特定关系(“结构”)的数据元素的集合。

数据结构包括逻辑结构和存储结构两个层次。

1.逻辑结构

(1) 集合结构

(2) 线性结构

(3) 树结构

(4) 图结构或网状结构

其中集合结构、树结构和图结构都属于非线性结构。

线性结构包括线性表、栈和队列、字符串、数组、广义表。

非线性结构包括树和二叉树、有向图和无向图。

2.存储结构

(1) 顺序存储结构

(2) 链式存储结构

1.4.1算法的定义及特性

算法是为了解决某类问题而规定的一个有限长的操作序列。

一个算法必须满足一下五个重要特性:

(1) 有穷性。一个算法必须总是在执行有穷步后结束,且每一步都必须在有穷时间内完成。

(2) 确定性。对于每种情况下所应执行的操作,在算法中都有确切的规定,不会产生二义性,使算法的执行者或阅读者都能明确其含义及如何执行。

(3) 可行性。算法中的所有操作都可以通过已经实现的基本操作运算执行有限次来实现。

(4) 输入。一个算法有零个或多个输入。

(5) 输出。一个算法有一个或多个输出。

1.4.3算法的时间复杂度

算法的时间复杂度取决于问题的规模和待处理数据的初态。

一般情况下,算法中基本语句重复执行的次数是时间规模n的某个函数f(n),算法的时间量度记作T(n)=O(f(n)),它表示随问题规模n的增大,算法执行时间的增长率和f(n)的增长率相同,称做算法的渐进时间复杂度,简称时间复杂度。

若f(n)=amnm+am-1nm-1+…+a1n+a0是一个m次多项式,则T(n)=O(nm)。

1.4.4算法的空间复杂度

关于算法的存储空间需求,类似于算法的时间复杂度,我们采用渐进空间复杂度作为算法所需存储空间的量度,简称空间复杂度,它也是问题规模n的函数,记作:S(n)=O(f(n))。

若算法执行时所需要的辅助空间相对于输入数据量而言是个常数,则称这个算法为原地工作,辅助空间为O(1)。

第2章 线性表

线性表特征:线性结构的基本特点是除第一个元素无直接前驱,最后一个元素无直接后继外,其他每个数据元素都有一个前驱和后继。

2.1线性表的定义和特点

由n(n>=0)个数据特性相同的元素构成的有限序列称为线性表(同一性、有穷性、有序性)。

线性表中元素的个数 n(n>=0)定义为线性表的长度,n=0时称为空表。

2.4线性表的顺序表示和实现

2.4.1线性表的顺序存储表示

线性表的顺序表示指的是用一组地址连续的存储单元依次存储线性表的数据元素,这种表示也称作线性表的顺序存储结构。

假设线性表的每个元素需占用l个存储单元,并以所占的第一个单元的存储地址(基地址)作为数据元素的存储起始位置。

线性表的第i个数据元素ai的存储位置为:LOC(ai)=LOC(a1)+(i-1)*l

第i+1个数据元素的存储位置和第i个数据元素的存储位置满足:LOC(ai+1)=LOC(ai)+l。

只要确定了存储线性表的起始位置,线性表中任一数据元素都可随机存取,所以线性表的顺序存储结构是一种随机存取的存储结构。

2.4.2顺序表中基本操作的实现

1.初始化

Status InitList(SqList &L)
{
    L.elem=new ElemType[MAXSIZE];  //为顺序表分配一个大小为MAXSIZE的数组空间
    if(!L.elem) exit(OVERFLOW);
    L.length=0;  //当前长度为0,为空表
    return OK;
}

2.取值

elem[i-1]单元存储第i个数据元素。

Status GetElem(SqList L,int I,ElemType &e)
{
    if(i<1||i>L.length) return ERROR;
    e=L.elem[i-1];
    return OK;
}

取值算法的时间复杂度为O(l)。

3.查找

int LocateElem(SqList L,ElemType e)
{
    for(i=0;i<L.length;i++)
       if(L.elem[i]==e) return i+1;  //查找成功,返回序号i+1
    return 0;  //查找失败,返回0
}

平均查找长度ASL=(n+1)/2,平均时间复杂度为O(n)。

4.插入

Status ListInsert(SqList &L,int i,ElemType e)
{
    if((i<1)||(i>L.length+1)) return ERROR;  //i不合法
    if(L.length==MAXSZIE) return ERROR;  //存储空间已满
    for(j=L.length-1;j>=i-1;j--)
       L.elem[j+1]=L.elem[j];  //插入位置后的元素后移
    L.elem[i-1]=e;  //新元素e放入第i个位置
    ++L.length;  //表长加1
    return OK;
}

所需移动元素次数的期望值Eins=n/2,插入算法的平均时间复杂度为O(n)。

5.删除

Status ListDelete(SqList &L,int i)
{
    if((i<1)||(i>L.length)) return ERROR;  //i不合法
    for(j=i;j<L.length-1;j++)
       L.elem[j-1]=L.elem[j];  //被删除元素之后的元素前移
    --L.length;  //表长减1
    return OK;
}

所移动元素的期望值Edel=(n-1)/2,删除算法的平均时间复杂度O(n)。

2.5线性表的链式表示和实现

线性表链式存储结构的特点是:用一组任意的存储单元存储线性表的数据元素(这组存储单元可以是连续的,也可以是不连续的)。元素本身的信息和指示其直接后继的信息组成数据元素ai的存储映像,称为结点。它包括两个域:其中存储数据元素信息的域称为数据域;存储直接后继存储位置的域称为指针域。由于此链表的每个结点中只包含一个指针域,故又称线性链表或单链表。

链表增加头节点的作用:(1)便于首元结点的处理(2)便于空表和非空表的统一处理。

2.5.2单链表基本操作的实现

操作特点:顺链操作,指针保留

1.初始化

Status InitList(LinkList &L)
{
    L=new LNode;
    L->next=NULL;
    return OK;
}

2.取值

Status GetElem(LinkList L,int i,ElemType &e)  //取序号为i的元素的值
{
    p=L->next;
    j=1;
    while(p&&j<i)  //p为空或p指向第i个元素跳出循环 
    {
       p=p->next;
       ++j;
    }
    if(!p||j>i) return ERROR;  //i>n或i<=0,不合法
    e=p->data;
    return OK;
}

平均查找长度ASL=(n-1)/2,平均时间复杂度为O(n)。

3.查找

分为按序号查找和按值查找,从链表的首元结点出发(只能顺链查找)。

按值查找:

LNode *LocateElem(LinkList L,ElemType e)
{
    p=L->next;
    while(p&&p->data!=e)  //p为空或p结点的数据域等于e跳出循环
       p=p->next;
    return p;
}

平均时间复杂度为O(n)。

4.插入

Status ListInsert(LinkList &L,int i,ElemType e)
{
    p=L;
    j=0;
    while(p&&(j<i-1))
        {p=p->next;++j;}
    if(!p||j>i-1) return ERROR;
    s=new LNode;
    s->data=e;
    s->next=p->next;
    p->next=s;
    return OK;
}

平均时间复杂度为O(n)。

5.删除

Status ListDelete(LinkList &L,int i)
{
    p=L;
    j=0;
    while((p->next)&&(j<i-1))  //查找第i-1个结点
	{p=p->next;++j;}
    if(!(p->next)||(j>i-1))  return ERROR; //i>n或i<1删除位置不合法
    q=p->next;
    p->next=q->next;  // p->next=p->next->next
    delete q;
    return OK;
}

平均时间复杂度为O(n)。

6.创建单链表

(1)前插法/头插法

void CreateList_H(LinkList &L,int n)
{
    L=new LNode;
    L->next=NULL;
    for(i=0;i<n;++i)
    {
	p=new LNode;
	cin>>p->data;
	p->next=L->next;
	L->next=p;  //p插入到头结点之后
    }
}

时间复杂度为O(n)。

(2)后插法/尾插法

Void CreateList_R(LinkList &L,int n)
{
    L=new LNode;
    L->next=NULL;
    r=L;  //尾指针r指向头结点
    for(i=0;i<n;++i)
    {
	p=new LNode;
	cin>>p->data;
	p->next=NULL;
	r->next=p;  //新节点p插入尾结点r之后
	r=p;  //r指向新的尾结点p
    }
}

时间复杂度为O(n)。

2.5.3循环链表

其特点是表中最后一个结点的指针域指向头结点(首尾相接)。

判别当前指针p是否指向表尾结点的终止条件为p!=L或p->next!=L。

2.5.4双向链表

在双向链表的结点中有两个指针域,一个指向直接后继,另一个指向直接前驱。

插入:

Status ListInsert_Dul(DuLinkList &L,int i,ElemType e)
{  //在第i个位置之前插入元素e
    if(!(p=GetElem_Dul(L,i))) return ERROR;  //第i个元素不存在
    s=new DulNode;
    s->data=e;
    s->prior=p->prior;
    p->prior->next=s;
    s->next=p;
    p->prior=s;
    return OK;
}

删除:

Status ListDelete_Dul(DuLinkList &L,int i)
{  //删除第i个元素
    if(!(p=GetElem_Dul(L,i))) return ERROR;  //第i个元素不存在
    p->prior->next=p->next;
    p->next->prior=p->prior;
    delete p;
    return OK;
}

2.6顺序表和链表的比较

2.6.1空间性能的比较

(1)存储空间的分配

当线性表的长度变化较大,难以预估存储规模时,宜采用链表作为存储结构。

(2)存储密度的大小

存储密度是指数据元素本身所占用的存储量和整个结点结构所占用的存储量之比。存储密度越大,存储空间的利用率就越高。顺序表的存储密度为1,而链表的存储密度小于1。

当线性表的长度变化不大,易于事先确定其大小时,为了节约存储空间,宜采用顺序表为存储结构。

2.6.2时间性能比较

(1)存取元素的效率

若线性表的主要操作是和元素位置紧密相关的这类取值操作,很少做插入或删除时,宜采用顺序表作为存储结构。

(2)插入和删除操作的效率

对于频繁进行插入或删除操作的线性表,宜采用链表作为存储结构。

第3章 栈和队列

运算的位置限定在表的端点。数据元素可是任意类型,但必须属于同一个数据对象,数据之间是线性关系。

3.1.1栈的定义和特点

栈是限定仅在表尾进行插入或删除操作的线性表。表尾端称为栈顶,表头端称为栈底。不含元素的空表称为空栈。栈又称为后进先出的线性表。

3.1.2队列的定义和特点

队列是一种先进先出的线性表。只允许在表的一端进行插入,而在另一端删除元素。在队列中,允许插入的一端称为队尾,允许删除的一端称为队头。

3.3栈的表示和操作的实现

3.3.2顺序栈的表示和实现

顺序栈,即利用一组地址连续的存储单元依次存放自栈底到栈顶的数据元素。指针top指示栈顶元素(top=0/-1表示空栈),指针base指示栈底元素。当top和base的值相等时,表示空栈。

栈空时,top和base的值相等;栈非空时,top始终指向栈顶元素的上一个位置。

1.初始化

Status InitStack(SqStack &S)
{
    S.base=new SElemType[MAXSIZE];
    if(!S.base) exit(OVERFLOW);
    S.top=S.base;
    S.stacksize=MAXSIZE;
    return OK;
}

2.入栈

Status Push(SqStack &S,SElemType e)
{
    if(S.top-S.base==S.stacksize) return ERROR;  //栈满
    *S.top++=e;  //*S.top=e; S.top++; e压入栈顶,栈顶指针加1
    return OK;
}

3.出栈

Status Pop(SqStack &S,SElemType &e)
{
    if(S.top==S.base) return ERROR;  //栈空
    e=*--S.top;  //S.top--;e=*S.top; 栈顶指针减1,栈顶元素赋给e
    return OK;
}

4.取栈顶元素

SElmType GetTop(SqStack &S)
{
    if(S.top!=S.base)  //栈非空
    return *(S.top-1);  //返回栈顶元素的值,栈顶指针不变
}

3.3.3链栈的表示和实现

以链表的头部作为栈顶,无需附加头结点。

  1. 链栈的初始化
Status InitStack(LinkStack &S)
{
    S=NULL;
    return OK;
}
  1. 链栈的入栈
Status Push(LinkStack &S,SElemType e)
{
    p=new StackNode;
    p->data=e;
    p->next=S;  //新节点插入栈顶
    S=p;  //栈顶指针指向p
    return OK;
}
  1. 链栈的出栈
Status Pop(LinkStack &S,SElemType &e)
{
    if(S==NULL) return ERROR;  //栈空
    e=S->data;  //栈顶元素赋给e
    p=S;
    S=S->next;  //修改栈顶指针
    delete p;
    return OK;
}

4.取链栈的栈顶元素

SElmType GetTop(LinkStack S)
{
    if(S!=NULL)
    return S->data;  //返回栈顶元素的值,栈顶指针不变
}

两栈共享技术:两栈共享栈空间。

3.4栈与递归

3.4.1采用递归算法解决的问题

若在一个函数、过程或者数据结构定义的内部又直接(或间接)出现定义本身的应用,则称它们是递归的。

一个递归算法必须包括终止条件和递归部分。

3.5队列的表示和操作的实现

3.5.2循环队列——队列的顺序表示和实现

附设两个整型变量front和rear分别指示队列头元素及队列尾元素的位置。

约定:初始化创建空队列时,令front=rear=0,每当插入新的队列尾元素时,尾指针rear增1;每当删除队列头元素时,头指针front增1。因此,在非空队列中,头指针始终指向队列头元素,而尾指针始终指向队列尾元素的下一个位置。

为解决上述约定的“假溢出”问题,将顺序队列变为一个环状的空间,称之为循环队列。头、尾指针 “依环状增1”的操作可用“模”运算来实现。

循环队列不能以头、尾指针的值是否相同来判断队列空间是“满”还是“空”,如何区分队满还是队空,通常有以下两种处理方法:

(1)少用一个元素空间,即队列空间大小为m时,有m-1个元素就认为是队满;当头、尾指针的值相同时,则认为队空。

队空的条件:Q.front==Q.rear

队满的条件:(Q.rear+1)%MAXSIZE==Q.front

(2)另设一个标志位以区别队列是“空”还是“满”。

1.循环队列的初始化

Status InitQueue(SqQueue &Q)
{
    Q.base=new QElemType[MAXSIZE];
    if(!Q.base) exit(OVERFLOW);
    Q.front=Q.rear=0;
    return OK;
}

2.求循环队列的长度

int QueueLength(SqQueue Q)
{
    return (Q.rear-Q.front+MAXSIZE)%MAXSIZE;
}

3.循环队列的入队

Status EnQueue(SqQueue &Q,QElemType e)
{
    if((Q.rear+1)%MAXSIZE==Q.front)  //队满
    return ERROR;
    Q.base[Q.rear]=e;  //e入队尾
    Q.rear=(Q.rear+1)%MAXSIZE;  //队尾指针加1
    return OK;
}

4.循环队列的出队

Status DeQueue(SqQueue &Q,QElemType &e)
{
    if(Q.front==Q.rear) return ERROR;  //队空
    e=Q.base[Q.front];  //取队头元素
    Q.front=(Q.front+1)%MAXSIZE;  //队头指针加1
    return OK;
}

5.取循环队列的队头元素

SElemType GetHead(SqQueue Q)
{
    if(Q.front!=Q.rear)  //队非空
    return Q.base[Q.front];  //取队头元素的值,队头指针不变
}

3.5.3链队——队列的链式表示和实现

一个链队需要两个分别指向队头和队尾的指针才能唯一确定,给链队天骄一个头结点,并令头指针始终指向头结点。

1.链队的初始化

Status InitQueue(LinkQueue &Q)
{
    Q.front=Q.rear=new QNode;  //队头和队尾的指针指向头结点
    Q.front->next=NULL;
    return OK;
}

2.链队的入队

Status EnQueue(LinkQueue &Q,QElemType e)
{
    p=new QNode;
    p->data=e;
    p->next=NULL;
    Q.rear->next=p;  //新节点插到队尾
    Q.rear=p;  //修改队尾指针
    return OK:
}

3.链队的出队

Status DeQueue(LinkQueue &Q,QElemType &e)
{
    if(Q.front==Q.rear) return ERROR;  //队空
    p=Q.front->next;
    e=p->data;  //取队头元素
    Q.front->next=p->next;  //修改头结点的指针域
    if(Q.rear==p) Q.rear=Q.front;  //最后一个元素被删,队尾指针指向头结点
    delete p;
    return OK;
}

4.取链队的队头元素

SElemType GetHead(LinkQueue Q)
{
    if(Q.front!=Q.rear)  //队非空
    return Q.front->next->data;  //返回队头元素,队头指针不变
}

第4章 串、数组和广义表

4.1串的定义

串(或字符串)是由零个或多个字符组成的有限序列。一般记为

s=“a1a2…an” (n>=0)

其中,s是串的名,用双引号括起来的字符序列是串的值;ai(1<=i<=n)(单字符)可以是字母、数字或其他字符;串中字符的数目n称为串的长度。零个字符的串称为空串,其长度为零。

串中任意个连续的字符组成的子序列称为该串的子串。包含子串的串相应地称为主串。子串在主串中的位置以子串的第一个字符在主串中的位置来表示。

称两个串是相等的,当且仅当这两个串的值相等。只有当两个串的长度相等,并且各个对应位置的字符都相等时才相等。

4.3串的类型定义、存储结构及其运算

4.3.2串的存储结构(顺序串,堆串,块链串)

1.串的顺序存储

#define MAXLEN 255  //串的最大长度
typedef struct{
char ch[MAXLEN+1];  //存储串的一维数组
int length;  //串的当前长度
}SString;

2.串的堆式顺序存储

typedef struct{
char *ch;  //若是非空串,则按串长分配存储区,否则ch为NULL
int length;  //串的当前长度
}HString;

3.串的链式存储

#define CHUNKSIZE 80  //可由用户定义的块大小
typedef struct Chunk{
char ch[CHUNKSIZE];
struct Chunk *next;
}Chunk;
typedef struct{
Chunk *head,*tail;  //串的头和尾指针
int length;  //串的当前长度
}LString;

存储密度小,运算处理方便,然而存储占用量大。

4.3.3串的模式匹配算法

子串的定位运算通常称为串的模式匹配或串匹配。

1.BF算法

int Index_BF(SString S,SString T,int pos)
{  //返回子串T在主串S中从第pos个字符开始第一次出现的位置
    i=pos;
    j=1;
    while(i<=S.length&&j<=T.length)
    {
	if(S.ch[i]==T.ch[j]) {++i;++j;}
	else {i=i-j+2;j=1;}
    }
    if(j>T.length) return i-T.length;
    else return 0;
}

设主串长度为n,子串的长度为m,最好的情况下匹配成功的平均比较次数为(n+m)/2,时间复杂度为O(n+m);最坏的情况下匹配成功的平均比较次数为m*(n-m+2)/2,时间复杂度为O(n*m)。

2.KMP算法

int Index_KMP(SString S,SString T,int pos)
{  //利用模式串T的next函数求T在主串S中从第pos个字符之后的位置
    i=pos;
    j=1;
    while(i<=S.length&&j<=T.length)
    {
	if(j==0&&S.ch[i]==T.ch[j]) {++i;++j;}
	else {j=next[j]};
    }
    if(j>T.length) return i-T.length;
    else return 0;
}

计算next函数值:

void get_next(SString T,int next[])
{
    i=1;
    next[1]=0;
    j=0;
    while(i<T.length)
    {
	if(j==0||T.ch[i]==T.ch[j]) {++i;++j;next[i]=j;}
	else j=next[j];
    }
}

计算next函数修正值:

void get_nextval(SString T,int nextval[])
{
    i=1;
    nextval[1]=0;
    j=0;
    while(i<T.length)
    {
	if(j==0||T.ch[i]==T.ch[j])
	{
	    ++i;
	    ++j;
	    if(T.ch[i]!=T.ch[j]) nextval[i]=j;
	    else nextval[i]=nextval[j];
	}
	else j=nextval[j];
    }
}

next数组计算:

next[1]=-1:next[i]为模式串第i-1个字符前与前缀匹配的字符串长度。

Next[1]=0:Next[i]=next[i]+1。

nextval数组计算:

nextval[1]=-1/0。

当T.ch[i]==T.ch[next[i]]时,nextval[i]=nextval[next[i]];

当T.ch[i]!=T.ch[next[i]]时,nextval[i]= next[i]。

4.4数组

数组时由类型相同的数据元素构成的有序集合。数组是线性表的原因:ai是单元素或者ai是带有结构信息的元素。

4.4.2数组的顺序存储

采用顺序存储结构表示数组比较合适。

对二维数组可有两种存储方式:一种是以列序为主序的存储方式;一种是以行序为主序的存储方式。

以行序为主序的存储结构:每个数据元素占L个存储单元,二维数组A[0…m-1,0…n-1]中任一元素aij的存储位置LOC(i,j)=LOC(0,0)+(n*i+j)L。

n维数组A[0…b1-1,0…b2-1,…,0…bn-1]的数据元素存储位置的计算公式:LOC(j1,j2,…,jn)=LOC(0,0,…,0)+(b2*…bnj1+b3*…bnj2+…+bn*jn-1+jn)L。

4.4.3特殊矩阵的压缩存储

1.对称矩阵(aij=aji)

以一维数组sa[n(n+1)/2]作为n阶对称矩阵A的存储结构,则sa[k]和矩阵元aij之间存在一一对应的关系:

在这里插入图片描述

2.三角矩阵

(1)上三角矩阵

sa[k]和矩阵元aij之间的对应关系为:

在这里插入图片描述

(2)下三角矩阵

sa[k]和矩阵元aij之间的对应关系为:

在这里插入图片描述

3.对称矩阵(带状矩阵)

按某个原则(或以行为主,或以对角线的顺序)将其压缩存储到一维数组上。

4.稀疏矩阵

其非零元较零元少,且分布没有一定规律,称之为稀疏矩阵。(三元组表示法或十字链表)

4.5广义表

4.5.1广义表的定义

广义表是线性表的推广,也成为列表。一般记作LS=(a1,a2,…,an).

其中,LS是广义表的名称,n是其长度。线性表中ai只限于是单个元素。而在广义表的定义中,ai可以是单个元素,也可以是广义表,分别称为广义表LS的原子和子表。用大写字母表示广义表的名称,用小写字母表示原子。

广义表的定义是一个递归的定义。

A=()——A是一个空表,其长度为零
B=(e)——B只有一个原子e,其长度为1
C=(a,(b,c,d))——C的长度为2,两个元素分别为原子a和子表(b,c,d)
D=(A,B,C)——D的长度为3,3个元素都是广义表,D=((),(e), (a,(b,c,d)))
E=(a,E)——这是一个递归的表

广义表的运算:

(1)取表头GetHead(LS):取出的表头为非空广义表的第一个元素,可以是一个单原子,也可以是一个子表。

(2)取表尾GetTail(LS):取出的表尾为除去表头之外,由其余元素构成的表。即表尾一定是一个广义表。

4.5.2广义表的存储结构

1.头尾链表的存储结构

表结点:

tag=1hptp

原子结点:

tag=0atom

(1)除空表的表头指针为空外,对任意非空广义表,其表头指针均指向一个表结点,且该结点中hp域指示广义表表头(或为原子结点,或为表结点),tp域指向广义表表尾(除非表尾为空,则指针为空,否则必为表结点)。

(2)容易分清列表中原子和子表所在层次。

(3)最高层的表结点个数即为广义表的长度。

2.扩展线性链表的存储结构

表结点:

tag=1hptp

原子结点:

tag=0atomtp

(1)tp指向同层的下一个

广义表的长度:n

广义表的深度:最多的括号层数

第5章 树和二叉树

树结构是一类重要的非线性数据结构(1:m,一对多的结构),树是以分支关系定义的层次结构。

5.1树和二叉树的定义

5.1.1树的定义

树是n(n>=0)个结点的有限集,它或为空树(n=0);或为非空树,对于非空树T:

(1)有且仅有一个称之为根的结点(没有直接前驱);

(2)除根结点以外的其余结点可分为m(m>0)个互不相交的有限集T1,T2,…,Tm,其中每一个集合本身又是一棵树,并且称为根的子树。

树的图解表示法:倒置树结构(树形表示法),文氏图表示法(嵌套集合形式),广义表形式(嵌套括号表示法),凹入表示法。

5.1.2树的基本术语

(1)结点:树中的一个单独单位。包含一个数据元素及若干指向其子树的分支。

(2)结点的度:结点拥有的子树数称为结点的度。

(3)树的度:树的度是树内各结点度的最大值。

(4)叶子:度为0的结点称为叶子或终端结点。

(5)非终端结点:度不为0的结点称为非终端结点或分支结点。

(6)双亲和孩子:结点的子树的根称为该结点的孩子,该结点称为孩子的双亲。

(7)兄弟:同一个双亲的孩子之间互称兄弟。

(8)祖先:从根到该结点所经分支上的所有结点。

(9)子孙:以某结点为根的子树中的任一结点都称为该结点的子孙。

(10)层次:结点的层次从根开始定义起,根为第一层,根的孩子为第二层。

(11)堂兄弟:双亲在同一层的结点互为堂兄弟。

(12)树的深度:树中结点的最大层次称为树的深度或高度。

(13)有序树和无序树:如果将树中结点的各子树看成从左至右是有次序的(即不能互换),则称该树为有序树,否则称为无序树。

(14)森林:是m(m>=0)棵互不相交的树的集合。

5.1.3二叉树的定义

二叉树是n(n>=0)个结点所构成的集合,它或为空树(n=0);或为非空树,对于非空树T:

(1)有且仅有一个称之为根的结点;

(2)除根结点以外的其余结点分为两个互不相交的子集T1和T2,分别称为T的左子树和右子树,且T1和T2本身又都是二叉树。

二叉树与树一样具有递归性质,二叉树与树的区别主要有以下两点:

(1)二叉树每个结点至多只有两棵子树(即二叉树中不存在度大于2的结点);

(2)二叉树的子树有左右之分,其次序不能任意颠倒。

5.4二叉树的性质和存储结构

5.4.1二叉树的性质

性质1:在二叉树的第i层上至多有2i-1个结点(i>=1)。

性质2:深度为k的二叉树至多有2k-1个结点(k>=1)。

性质3:对任何一棵二叉树T,如果其终端结点数为n0,度为2的结点数为n2,则n0=n2+1。

满二叉树:深度为k且含有2k-1个结点的二叉树。

满二叉树特点:每一层上的结点数都是最大结点数,即每一层i的结点数都具有最大值2i-1。

完全二叉树:深度为k,有n个结点的二叉树,当且仅当其每一个结点都与深度为k的满二叉树中编号从1至n的结点一一对应时,称之为完全二叉树。

完全二叉树的特点:

(1)叶子结点只可能在层次最大的两层上出现。

(2)对任一结点,若其右分支下的子孙的最大层次为l,则其左分支下的子孙的最大层次必为l或l+1.

性质4:具有n个结点的完全二叉树的深度为⌊log2n⌋+1。

性质5:如果对一棵有n个结点的完全二叉树(其深度为⌊log2n⌋+1)的结点按层序编号(从第1层到第⌊log2n⌋+1层,每层从左到右),则对任一结点i(1<=i<=n),有

(1)如果i=1,则结点i是二叉树的根,无双亲,如果i>1,则其双亲parent(i)是结点⌊i/2⌋。

(2)结点i的左孩子lchild是结点2i,如果2i>n,则结点i无左孩子(结点i为叶子结点)。

(3)结点i的右孩子rchild是结点2i+1,如果2i+1>n,则结点i无右孩子。

5.4.2二叉树的存储结构

1.顺序存储结构

#define MAXSIZE 100  //二叉树的最大结点数
typedef TElemType SqBiTree[MAXSIIZE];  //0号单元存储根节点
SqBiTree bt;

2.链式存储结构

typedef struct BiTNode{
	TElemType data;  //结点数据域
	struct BiTNode *lchild,*rchild;  //左右孩子指针
}BiTNode,*BiTree;

5.5遍历二叉树和线索二叉树

5.5.1遍历二叉树

遍历二叉树是指按某条搜索路径巡防树中每个结点,使得每个结点均被访问一次,而且仅被访问一次(统计叶子结点)。

若限定先左后右,则有三种情况,分别称之为先(根)序遍历、中(根)序遍历和后(根)序遍历。

表达式的前缀表示(波兰式)、中缀表示和后缀表示(逆波兰式)。计算:波兰式从左到右扫描到第一个运算符号(#)后跟两个数据(AB),计算A#B替代#AB,再从左到右扫描;逆波兰式计算从右到左扫描。

中序遍历的递归算法:

void InOrderTraverse(BiTree T)
{
	if(T)  //若二叉树非空
	{
		InOrderTraverse(T->lchild);  //中序遍历左子树
		cout<<T->data;  //访问根节点
		InOrderTraverse(T->rchild);  //中序遍历右子树
	}
}

中序遍历的非递归算法:

void InOrderTraverse(BiTree T)
{
	InitStack(S);
	p=T;
	q=new BiiTNode;
	while(p||!StackEmpty(S))
	{
		if(p)  //p非空
		{
			Push(S,p);  //根指针进栈
			p=p->lchild;  //遍历左子树
		}
		else  //p为空
		{
			Pop(S,q);  //退栈
			cout<<q->data;  //访问根结点
			p=q->rchild;  //遍历右子树
		}
	}
}

二叉树遍历算法的应用:

先序遍历的顺序建立二叉链表:

void CreateBiTree(BiTree &T)
{
	cin>>ch;
	if(ch==’#’) T=NULL;  //递归结束,建立空树
	else  //递归创建二叉树
	{
		T=new BiTNode;  //生成根节点
		T->data=ch;  //根节点数据域置为ch
		CreateBiTree(T->lchild);  //递归创建左子树
		CreateBiTree(T->rchild);  //递归创建右子树
	}
}

复制二叉树:

void Copy(BiTree T,BiTree &NewT)
{
	if(T==NULL)  //如果是空树,递归结束
	{
		NewT=NULL;
		return ;
	}
	else
	{
		NewT=new BiTNode;
		NewT->data=T->data;  //复制根节点
		Copy(T->lchild,NewT->lchild);  //递归复制左子树
		Copy(T->rchild,NewT->rchild);  //递归复制右子树
	}
}

计算二叉树的深度:

int Depth(BiTree T)
{
	if(T==NULL) return 0;  //空树深度为0
	else
	{
		m=Depth(T->lchild);  //递归计算左子树的深度为m
		n=Depth(T->rchild);  //递归计算右子树的深度为n
		if(m>n) return(m+1);  //二叉树的深度为m与n的较大者加1
		else return(n+1);
	}
}

统计二叉树中结点的个数:

int NodeCount(BiTree T)
{
	if(T==NULL) return 0;  //空树,结点个数为0
	else return NodeCount(T->lchild)+NodeCount(T->rchild)+1;  
	//否则结点个数为左子树结点个数+右子树结点个数+1(本结点)
}

5.5.2线索二叉树

线索二叉树的结点形式:

lchildLTagdataRTagrchild

其中:
在这里插入图片描述
在这里插入图片描述


typedef struct BiThrNode
{
	TElemType data;
	struct BiThrNode *lchild,*rchild;  //左右孩子指针
	int LTag,RTag;  //左右标志
}BiThrNode,*BiThrTree;

以这种结点结构构成的二叉链表作为二叉树的存储结构,叫做线索链表,其中指向结点前驱和后继的指针,叫做线索。加上线索的二叉树称之为线索二叉树。

引入二叉线索树的目的是加快查找结点前驱或后继的速度。

构造线索二叉树:

线索二叉树构造的实质是将二叉链表中的空指针改为指向前驱或后继的线索。

对二叉树按不同的遍历次序进行线索化,可以得到不同的线索二叉树,包括先序线索二叉树、中序线索二叉树和后序线索二叉树。

画线索二叉树:线索指针用虚线画出,指向前驱或后继。

5.6树和森林

5.6.1树的存储结构

1.双亲表示法

以一组连续的存储单元存储树的结点,每个结点除了数据域data外,还附设一个parent域用以指示其双亲结点的位置。

很容易求树的根,但求结点的孩子时需要遍历整个结构。

2.孩子表示法

用多重链表,即每个结点有多个指针域,其中每个指针指向一棵子树的根节点。

3.孩子兄弟法

又称二叉树表示法,或二叉链表表示法,即以二叉链表做树的存储结构。链表中结点的两个链域分别指向该结点的第一个孩子结点和下一个兄弟结点。

5.6.2森林与二叉树的转换

1.树转换成二叉树

(1)树中所有相邻兄弟之间架一条连线

(2)对树中的每个结点,只保留其与第一个孩子结点之间的连线,删去与其他孩子结点的连线

(3)以树的根结点为轴心,整理

2.二叉树还原成树或森林

(1)若某结点是其双亲的左孩子,则把该结点的右孩子,右孩子的右孩子…都与该结点的双亲结点用线连起来

(2)删掉原二叉树中所有双亲结点与右孩子结点的连线

3.森林转换成二叉树

(1)森林中的每棵树转换成二叉树(根T1,T2,T3…)

(2)T2为T1的右孩子,T3为T2的右孩子…

树的孩子兄弟表示法=二叉树的链表表示法

5.6.3树和森林的遍历

1.树的遍历

一种是先根(次序)遍历树,即:先访问树的根节点,然后依次先根遍历根的每棵子树;

一种是后根(次序)遍历,即:先依次后跟遍历每棵子树,然后访问根结点。

2.森林的遍历

(1)先序遍历森林(根-子森林-余森林)

访问森林中第一棵树的根结点;先序遍历第一棵树的根结点的子树森林;先序遍历除去第一棵树之后剩余的树构成的森林。

(2)中序遍历森林(子森林-根-余森林)

中序遍历第一棵树的根结点的子树森林;访问第一棵树的根结点;中序遍历除去第一棵树之后剩余的树构成的森林。

(3)后序遍历森林(子森林-余森林-根)

5.7哈夫曼树及其应用

5.7.1哈夫曼树的基本概念

哈夫曼树又称最优树,是一类带权路径长度最短的树。

(1)路径:从树中一个结点到另一个结点之间的分支构成这两个结点之间的路径。

(2)路径长度:路径上的分支数目称作路径长度。

(3)树的路径长度:从树根到每一结点的路径长度之和。

(4)权:赋予某个实体的一个量。对于有结点权和边权。如果一棵树中的结点上带有权值,则对应的就有带权树等概念。

(5)结点的带权路径长度:从该结点到树根之间的路径长度与结点上权的乘积。

(6)树的带权路径长度:树中所有叶子结点的带权路径长度之和,记作WPL

(7)哈夫曼树:带权路径长度WPL最小的二叉树称作最优二叉树或哈夫曼树。

在哈夫曼树中,权值越大的结点离根节点越近。

5.7.2哈夫曼树的构造算法

1.哈夫曼树的构造过程

(1)给定的n个权值{w1,w2,…,wn},构造n棵只有根节点的二叉树,这n棵二叉树构成一个森林F。

(2)在森林F中选取两棵根节点的权值最小的树作为左右子树构造一棵新的二叉树,且置新的二叉树的根节点的权值为其左、右子树上根节点的权值之和。

(3)在森林F中删除这两棵树,同时将新得到的二叉树加入F中。

(4)重复(2)和(3),直到F只含有一棵树为止。这棵树便是哈夫曼树。

2.哈夫曼算法的实现

哈夫曼树结点的形式(静态三叉链表):

weightparentlchildrchild

哈夫曼树中没有度为1的结点,一棵有n个叶子结点的哈夫曼树共有2n-1个结点。可以存储在一个大小为2n-1的一维数组中。

typedef struct{
	int weight;
	int parent,lchild,rchild;
}HTNode,*HuffmanTree;

哈夫曼树的存储结构HT的初态和终态示例:
在这里插入图片描述
在这里插入图片描述

构造哈夫曼树:

void CreateHuffmanTree(HuffmanTree &HT,int n)
{
	if(n<=1) return ;
	m=2*n-1;
	HT=new HTNode[m+1];
	for(i=1;i<=m;++i)
		{HT[i].parent=0;HT[i].lchild=0; HT[i].rchild=0;}
	for(i=1;i<=n;++i)
		cin>>HT[i].weight;  //输入前n个单元叶子节点的权值
	for(i=n+1;i<=m;++i)
	{
		Select(HT,i-1,s1,s2);  //返回权值最小的结点s1,s2
		HT[s1].parent=i; HT[s2].parent=i;  //s1,s2的双亲域改为i
		HT[i].lchild=s1;HT[i].rchild=s2;  //s1,s2分别作为i的左右孩子
		HT[i].weight= HT[s1].weight+ HT[s2].weight;   //i的权值为左右孩子权值之和
	}
}

5.7.3哈夫曼编码

(1)前缀编码:一个编码方案中,任一个编码都不是其他任何编码的前缀(最左子串),则称编码是前缀编码。

(2)哈夫曼编码:每个左分支赋予0,右分支赋予1,则从根到每个叶子的路径上,各分支的赋值分别构成一个二进制串,该二进制串就称为哈夫曼编码。

哈夫曼编码满足两个性质:

性质1:哈夫曼编码是前缀编码。

性质2:哈夫曼编码是最优前缀编码。

哈夫曼编码的算法实现:

typedef char **HuffmanCode;
void CreatHuffmanCode(HuffmanTree HT,HuffmanCode &HC,int n)
{
	HC=new char*[n+1];
	cd=new char[n];
	cd[n-1]=’\0;
	for(i=1;i<=n;i++)
	{
		start=n-1;
		c=i;f=HT[i].parent;
		while(f!=0)
		{
			--start;
			if(HT[f].lchild==c) cd[start]=0;
			else cd[start]=1;
			c=f;f=HT[f].parent;
		}
		HC[i]=new char[n-start];
		strcpy(HC[i],&cd[start]);
	}
	delete cd;
}

第6章图

图:结点间邻接关系任意(多对多)。

6.1.1图的定义

图G由两个集合V和E组成,记为G=(V,E),其中V是顶点的有穷非空集合,E是V中顶点偶对的有穷集合,这些顶点的偶对称为边。E(G)可以为空集,则图G只有顶点而没有边。

在有向图中,顶点对<x,y>是有序的,它称为从顶点x到顶点y的一条有向边。<x,y>与<y,x>是不同的两条边。顶点对用一对尖括号括起来,x是有向边的始点,y是有向边的终点。<x,y>也称作一条弧,则x为弧尾,y为弧头。

无向图,顶点(x,y)是无序的,无向图的顶点对用一对圆括号括起来。

6.1.2图的基本术语

(1)子图:假设有两个图G=(V,E)和G’=(V’,E’),如果V’⊆V且E’⊆E,则称G’为G的子图。

(2)无向完全图和有向完全图:无向图若具有n(n-1)/2条边,则称为无向完全图。有向图若具有n(n-1)条弧,则称为有向完全图。

(3)稀疏图和稠密图:少边少弧(e<nlog2n)稀疏图,反之稠密图。

(4)权和网:每条边可以标上具有某种含义的数值,该数值称为该边上的权。

这种带权的图通常称为网。

(5)邻接点:对于无向图G,如果图的边(v,v’)∈E,则称顶点v和v’互为邻接点,即v和v’相邻接。边(v,v’)依附于顶点v和v’,或者说边(v,v’)与顶点v和v’相关联。

(6)度、入度和出度:顶点v的度是指和v相关联的边的数目,记为TD(v)。对于有向图,顶点v的度分为入度和出度。入度是以顶点v为头的弧的数目,记为ID(v);出度是以顶点v为尾的弧的数目,记为OD(v);顶点v的度为TD(v)=ID(v)+OD(v)。

(7)路径和路径长度:无向图G中,从顶点v到顶点v’的路径是一个顶点序列(v=vi,0,vi,1,…,vi,m=v’),其中(vi,j-1,vi,j) ∈E,1<=j<=m。若G是有向图,则路径也是有向的,顶点序列满足<vi,j-1,vi,j> ∈E,1<=j<=m。路径长度是一条路径上经过的边或弧的数目。

(8)回路或环:第一个顶点和最后一个顶点相同的路径称为回路或环。

(9)简单路径、简单回路或简单环:序列中顶点不重复出现的路径称为简单路径。除了第一个顶点和最后一个顶点外,其余顶点不重复出现的回路,称为简单回路或简单环。

(10)连通、连通图和连通分量:在无向图G中,如果从顶点v到顶点v’有路径,则称v和v’是连通的。如果对于图中任意两个顶点都是连通的,则称G是连通图。所谓连通分量,指的是无向图中的极大连通子图。

(11)强连通图和强连通分量:在有向图G中,如果对于每一对vi,vj∈V,vi≠vj,从vi到vj和从vj到vi都存在路径,则称G是强连通图(至少n条边)。有向图中的极大强连通子图称作有向图的强连通分量。

(12)连通图的生成树:一个极小连通子图,它包含有图中全部顶点,但只有足以构成一棵树的n-1条边,这样的连通子图称为连通图的生成树。

(13)有向树和生成森林:有一个顶点的入度为0,其余顶点的入度均为1的有向图称为有向树。一个有向树的生成森林是由若干棵有向树组成,含有图中全部顶点,但只有足以构成若干棵不相交的有向树的弧。

6.4图的存储结构

6.4.1邻接矩阵

邻接矩阵是表示顶点之间相邻关系的矩阵。

设G(V,E)是具有n个顶点的图,则G的邻接矩阵是具有如下性质的n阶方阵:
在这里插入图片描述

若G是网,则邻接矩阵可以定义为:

在这里插入图片描述

用邻接矩阵表示法表示图,除了一个用于存储邻接矩阵的二维数组外,还需要用一个一维数组来存储顶点信息。

对于无向图,邻接矩阵第i行元素之和就是顶点i的度。

对于有向图,第i行元素之和就是顶点i的出度,第i列元素之和就是顶点i的入度。

6.4.2邻接表

邻接表是图的一种链式存储结构(只存储图中有关联的边的信息)。在邻接表中,对图中每个顶点vi建立一个单链表,把与vi相邻接的顶点放在这个链表中。这样的邻接表由两部分组成:表头结点和边表。

(1)表头结点表:每个顶点信息与其边链表的头指针构成。表头结点包括数据域和链域。数据域用于存储vi的名称或其他有关信息,链域用于指向链表中第一个结点。

(2)边表:由表示图中顶点间关系的(有向图n个/无向图2n个)边链表组成。边链表中边结点包括邻接点域、数据域和链域。邻接点域指示与顶点vi邻接的点在图中的位置

表头结点:

datafirstarc

边结点:

adjvexinfonextarc

有向图的逆邻接表,即对每个顶点vi建立一个链接所有进入vi的边的表(入度表)。

有向图邻接表,无向图邻接表,有向图逆邻接表:

在这里插入图片描述

6.4.3十字链表

十字链表是有向图的另一种链式存储结构。可以看成是将有向图的邻接表和逆邻接表结合起来得到的一种链表。

6.4.4邻接多重表

邻接多重表是无向图的另一种链式存储结构。

6.5图的遍历

图的遍历是从图中某一顶点出发,按照某种方法对图中所有顶点访问且仅访问一次。

为了避免同一顶点被访问多次,设一个辅助数组visited[n](访问标志数组),其初值为false或0,一旦访问了顶点vi,便置visited[i]为true或1。

6.5.1深度优先搜索

深度优先搜索(DFS)遍历类似于树的先序遍历(借助于栈结构实现(递归))。过程:

(1)从图中某个顶点v出发,访问v

(2)找出刚访问过的顶点的第一个未被访问的邻接点,访问该结点。以该结点为新结点,重复此步骤,直至刚访问过的顶点没有未被访问的邻接点为止。

(3)返回前一个访问过且仍有未被访问的邻接点的顶点,找出该顶点的下一个未被访问的邻接点,访问该顶点。

(4)重复(2)和(3),直至图中所有顶点都被访问过,搜索结束。

深度优先搜索遍历连通图:

bool visited[MVNum];
void DFS(Graph G,int v)
{
	cout<<v;visited[v]=true;  //访问第v个顶点
	for(w=FirstAdjVex(G,v);w>=0;w=NextAdjVex(G,v,w))
	{  //FirstAdjVex(G,v)表示v的第一个邻接点
	   //NextAdjVex(G,v,w)表示v相对于w的下一个邻接点,w>=0表示存在邻接点
		if(!visited[w])
		{
			Printf(v,w);  //打印遍历路径
			DFS(G,w);  //对v的尚未访问的邻接顶点w递归调用DFS
		}
	}
}

深度优先搜索遍历非连通图:

void DFSTraverse(Graph G)
{
	for(v=0;v<G.vexnum;++v) visited[v]=false;  //访问标志数组初始化
	for(v=0;v<G.vexnum;++v)  //循环调用算法DFS
	if(!visited[v]) DFS(G,v);  //对尚未访问的顶点调用DFS
}

采用邻接矩阵表示图的深度优先搜索遍历:

void DFS_AM(AMGraph G,int v)
{
	cout<<v;visited[v]=true;  //访问第v个顶点
	for(w=0;w<G.vexnum;w++)  //依次检查矩阵v所在的行
	if((G.arcs[v][w]!=0)&&(!visited[w]))
	DFS_AM(G,w);
	//G.arcs[v][w]!=0表示w是v的邻接点,如果w未访问,则递归调DFS_AM
}

采用邻接表表视图的深度优先搜索遍历:

void DFS_AL(ALGraph G,int v)
{
	cout<<v;visited[v]=true;  //访问第v个顶点
	p=G.vertices[v].firstarc;  //p指向v的边链表的第一个边结点
	while(p!=NULL)  //边结点非空
	{
		w=p->adjvex;  //表示w是v的邻接点
		if(!visited[w]) DFS_AL(G,w);  //如果w未访问,则递归调用DFS_AL
		p=p->nextarc;  //p指向下一个边结点
	}
}

6.5.2广度优先搜索

广度优先搜索(BFS)遍历类似于树的按层次遍历的过程(借助于队列结构实现)。过程:

(1)从图中的某个顶点v出发,访问v。

(2)依次访问v的各个未曾访问过的邻接点。

(3)分别从这些邻接点出发依次访问它们的邻接点,并使“先被访问的顶点的邻接点”先于“后被访问的顶点的邻接点”被访问。重复步骤(3),直至图中所有已被访问的顶点的邻接点都被访问到。

广度优先搜索遍历连通图:

void BFS(Graph G,int v)
{
	cout<<v;visited[v]=true;  //访问v
	InitQueue(Q);  //辅助队列Q初始化
	EnQueue(Q,v);  //v进队
	while(!QueueEmpty(Q))  //队列非空
	{
		DeQueue(Q,u);  //队头元素出队并置为u
		for(w=FirstAdjVex(G,u);w>=0;w=NextAdjVex(G,u,w))
		{  //FirstAdjVex(G,u)表示u的第一个邻接点
		   //NextAdjVex(G,u,w)表示u相对于w的下一个邻接点,w>=0表示存在邻接点
			if(!visited[w])
			{
				cout<<w;visited[w]=true;  //访问w
				Printf(u,w);  //打印访问路径(生成树结点)
				EnQueue(Q,w);  //w进队
			}
		}
	}
}

无向图的连通分量:调用搜索过程的次数就是该图连通分量的个数。

连通图:仅调用一次搜索过程。

非连通图:需调用多次搜索过程。

6.6图的应用

6.6.1最小生成树(无向图)

在一个连通网的所有生成树中,各边的代价之和最小的那颗生成树称为该连通网的最小代价生成树,简称为最小生成树。

1.普里姆算法

加点法:

假设N=(V,E)是连通网,TE是N上最小生成树中边的集合。

(1)U={u0}(u0∈V),TE={}。

(2)在所有u∈U,v∈V-U的边(u,v)∈E中找一条权值最小的边(u0,v0)并入集合TE,同时v0并入U。

(3)重复(2),直至U=V为止。
在这里插入图片描述

2.克鲁斯卡尔算法

加边法:

假设连通网N=(V,E),将N中的边按权值从小到大的顺序排列。

(1)初始状态为只有n个顶点而无边的非连通图T=(V,{}),图中每个顶点自成一个连通分量。

(2)在E中选择权值最小的边,若该边依附的顶点落在T中不同的连通分量上(即不形成回路),则将此边加入到T中,否则舍去此边而选择下一条权值最小的边。

(3)重复(2),直至T中所有顶点都在同一连通分量上为止。

在这里插入图片描述

6.6.2最短路径

路径上的第一个顶点为源点,最后一个顶点为终点。

两种最常见的最短路径问题:一种是求从某个源点到其余各顶点的最短路径,另一种是求每一对顶点之间的最短路径。

1.从某个源点到其余各顶点的最短路径

迪杰斯特拉算法:

原来v0到vi的最短路径长度为D[i],加入vk之后,以vk作为中间顶点的“中转”路径长度为:D[k]+G.arcs[k][i],若D[k]+G.arcs[k][i]<D[i],则用D[k]+G.arcs[k][i]取代D[i]。

时间复杂度为O(n2)。

2.每一对顶点之间的最短路径

求解每一对顶点之间的最短路径有两种方法:其一是分别以图中的每个顶点为源点共调用n次迪杰斯特拉算法;其二是采用弗洛伊德算法。时间复杂度为O(n3)。

弗洛伊德算法:

if(D[i][k]+D[k][j]<D[i][j]) D[i][j]=D[i][k]+D[k][j];

6.6.3拓扑排序

特性:先行关系具有可传递性;拓扑序列不唯一。

1.AOV-网

用顶点表示活动,用弧表示活动间的优先关系的有向图称为顶点表示活动的网,简称AOV-网。

AOV-网中,不应该出现有向环。对给定的AOV-网应首先判定网中是否存在环。检测的办法是对有向图的顶点进行拓扑排序,若网中所有顶点都在它的拓扑有序序列中,则该AOV-网中必定不存在环。

拓扑排序就是将AOV-网中所有顶点排成一个线性序列,该序列满足:若在AOV-网中由顶点vi到顶点vj有一条路径,则在该线性序列中的顶点vi必定在顶点vj之前。

2.拓扑排序的过程

(1)在有向图中选一个无前驱的顶点且输出它。

(2)从图中删除该顶点和所有以它为尾的弧。

(3)重复(1)和(2),直至不存在无前驱的顶点。

(4)若此时输出的顶点数小于有向图中的顶点数,则说明有向图中存在环,否则输出的顶点序列即为一个拓扑排序。

基于邻接矩阵表示的存储结构:

A为有向图G的邻接矩阵,则有

(1)找G中无前驱的顶点——在A中找到值全为0的列。

(2)删除以i为起点的所有弧——将矩阵中i对应的行置为全0。

6.6.4关键路径

1.AOE-网

AOE-网,即以边表示活动的网。AOE-网是一个带权的有向无环图,其中,顶点表示事件,弧表示活动,权表示活动持续的时间。

网中只有一个入度为零的点,称为源点,也只有一个出度为零的点,称作汇点。在AOE-网中,一条路径各弧上的权值之和称为该路径的带权路径长度。要估算整项工程完成的最短时间,就是要找一条从源点到汇点的带权路径长度最长的路径,称为关键路径。关键路径上的活动叫做关键活动。

如何确定关键路径,首先定义4个描述量:

(1)事件vi的最早发生时间ve(i)

ve(i)是从源点到vi的最长路径长度。

(2)事件vi的最迟发生时间vl(i)

vi的最迟发生时间:其后继事件vk的最迟发生时间减去活动<vi,vk>的持续时间的最小值。

(3)活动ai=<vj,vk>的最早开始时间e(i)

活动ai的最早开始时间等于事件vj的最早发生事件ve(j)。

(4)活动ai=<vj,vk>的最晚开始时间l(i)

活动ai的最晚开始时间l(i)等于事件vk的最迟发生时间vl(k)减去活动ai的持续时间wj,k

一个活动ai的最迟开始时间l(i)和其最早开始时间e(i)的差值l(i)-e(i)是该活动完成的时间余量。当l(i)-e(i)=0,即l(i)=e(i)时的活动ai是关键活动。

第7章查找

7.1查找的基本概念

(1)查找表
查找表是由同一类型的数据元素(或记录)构成的集合。查找表是一种非常灵活的数据结构,可以利用其他的数据结构来实现。

(2)关键字
关键字是数据元素(或记录)中某个数据项的值,用它可以标识一个数据元素(或记录)。若此关键字可以唯一地标识一个记录,则称此关键字为主关键字。反之,称用以识别若干记录的关键字为次关键字。

(3)查找
查找是指根据给定的某个值,在查找表中确定一个其关键字等于给定值的记录或数据元素。若表中存在这样的一个记录,则称查找成功;若表中不存在关键字等于给定值的记录,则称查找不成功。

(4)动态查找表和静态查找表
若在查找的同时对表做修改操作,则相应的表称之为动态查找表,否则称之为静态查找表。

(5)平均查找长度
为确定记录在查找表中的位置,需和给定值进行比较的关键字个数的期望值,称为查找算法在查找成功时的平均查找长度(ASL)。对于含有n个记录的表,查找成功时的平均查找长度为ASL=∑ni=1PiCi,其中,Pi为查找表中第i个记录的概率,Ci为找到第i个元素的比较次数。
可用平均查找长度来衡量查找算法的性能。

7.2线性表的查找

7.2.1顺序查找

查找过程:从表的一端开始,依次将记录的关键字和给定值进行比较,若某个记录的关键字和给定值相等,则查找成功。反之,若扫描整个表后,仍未找到关键字和给定值相等的记录,则查找失败。
ASL=(n+1)/2,时间复杂度为O(n)。

7.2.2折半查找
折半查找也称二分查找。折半查找要求线性表必须采用顺序存储结构,而且表中元素按关键字有序排列。

查找过程:(有序表递增)从表的中间记录开始,如果给定值和中间记录的关键字相等,则查找成功;如果给定值大于或者小于中间记录的关键字,则在表中大于或小于中间记录的那一半中查找,这样重复操作,直到查找成功,如果在某一步中查找区间为空,则代表查找失败。
折半查找过程可用二叉树来描述。树中每一结点对应表中一个记录,结点值是记录在表中的位置序号。把当前查找区间的中间位置作为根,左子表和右子表分别作为根的左子树和右子树,由此得到的二叉树称为折半查找的判定树。树的深度为查找对应记录的次数。对于长度为n的有序表,折半查找法在查找成功时进行比较的关键字个数至多为⌊log2n⌋+1。

查找成功时折半查找的平均查找长度为ASL=[(n+1)/n]log2 (n+1)-1,时间复杂度为O(log2n)。

折半查找的优点是:比较次数少,查找效率高。其缺点是:对表结构要求高,只能用于顺序存储的有序表。

7.2.3分块查找
分块查找又称索引顺序查找(块内无序,块间有序)。建立索引表,对每个子表建立索引项,其中包括两项内容:关键字项(其值为该子表内的最大关键字)和指针项(指示该子表的第一个记录在表中的位置)。索引表按关键字有序。
分块查找分两步进行:先确定待查记录所在的块(子表),然后在块中顺序查找。
若进行分块查找,将长度为n的表均匀地分成b块,每块含有s个记录,即b=⌈n/s⌉。若用顺序查找确定所在块,则分块查找的平均查找长度为ASLbs=(1/2) (n/s+s)+1。若用折半查找确定所在块,则分块查找的平均查找长度为ASL`bs=log2 (n/s+1)+s/2。

分块查找的优点:只要找到该元素对应的块,就可以在块内进行插入删除运算。由于块内是无序的,无需进行大量移动。如果线性表既要快速查找又经常动态变化,则可采用分块查找。其缺点是:要增加一个索引表的存储空间并对初始索引表进行排序运算。

7.3树表的查找
将待查表组织成特定树,在树结构上实现查找。

7.3.1二叉排序树

1.二叉排序树的定义
二叉排序树或者是一棵空树,或者是具有下列性质的二叉树:

(1)若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
(2)若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
(3)它的左、右子树也分别为二叉排序树。

2.二叉排序树的查找
平均查找长度:最好log2n,最差(n+1)/2。

3.二叉排序树的插入
时间复杂度O(log2n)。

4.二叉排序树的创建
时间复杂度O(nlog2n)。

5.二叉排序树的删除
被删结点缺右子树用左孩子填补;
被删结点缺左子树用右孩子填补;
被删结点左右子树都存在,在左子树上找中序遍历的最后一个结点填补。

7.3.2平衡二叉树
1.平衡二叉树的定义
二叉排序树查找算法的性能取决于二叉树的结构,而二叉排序树的形状则取决于数据集。树的高度越小,查找速度越快。
平衡二叉树(一种特殊类型的二叉排序树),又称AVL树。
平衡二叉树或者是空树,或者是具有如下特征的二叉排序树:
(1)左子树和右子树的深度之差的绝对值不超过1;
(2)左子树和右子树也是平衡二叉树。
二叉树上结点的平衡因子定义为该结点左子树和右子树的深度之差,则平衡二叉树上所有结点的平衡因子只可能是-1、0和1。

2.平衡二叉树的平衡调整方法
创建一棵平衡二叉树:插入结点时按二叉排序树处理,若插入结点后破环了平衡二叉树的特性,需对平衡二叉树进行调整。调整方法:找到离插入结点最近且平衡因子绝对值超过1的祖先结点,以该结点为根的子树称为最小不平衡子树,可将重新平衡的范围局限于这颗子树。
假设最小不平衡子树的根结点为A,则失去平衡后进行调整的规律可归纳为下列4种情况。

(1)LL型:由于在A左子树根节点的左子树上插入结点,A的平衡因子由1增至2,致使以A为根的子树失去平衡,则需进行一次向右的顺时针旋转操作。
在这里插入图片描述
(2)RR型:由于在A右子树根节点的右子树上插入结点,A的平衡因子由-1变成-2,致使以A为根的子树失去平衡,则需进行一次向左的逆时针旋转操作。
在这里插入图片描述
(3)LR型:由于在A左子树根节点的右子树上插入结点,A的平衡因子由1增至2,致使以A为根的子树失去平衡,则需进行两次旋转操作。第一次对B及其右子树进行逆时针旋转,C转上去成为B的根,这时变成了LL型,所以第二次进行LL型的顺时针旋转即可恢复平衡。
在这里插入图片描述
(4)RL型:由于在A右子树根节点的左子树上插入结点,A的平衡因子由-1变成-2,致使以A为根的子树失去平衡,则旋转方法和LR型相对称。也需进行两次旋转,先顺时针右旋,再逆时针左旋。

7.3.3B-树
适用于外查找的平衡多叉树——B-树(多路平衡查找树),是一种组织和维护外存文件系统非常有效的数据结构。

1.B-树的定义
一棵m阶的B-树,或为空树,或为满足下列特性的m叉树:
(1)树中每个结点至多有m棵子树;
(2)若根节点不是叶子节点,则至少有两颗子树;
(3)除根之外的所有非终端结点至少有⌈m/2⌉棵子树;
(4)所有的叶子结点都出现在同一层次上,并且不带信息,通常称为失败结点(失败结点并不存在,指向这些结点的指针为空。引入失败结点是为了便于分析B-树的查找性能);
(5)所有的非终端结点最多有m-1个关键字,B-树的结构如下所示:

nP0K1P1K2P2KnPn

其中,ki为关键字,且ki<Ki+1;Pi为指向子树根节点的指针,且指针Pi-1所指子树中所有结点的关键字均小于ki,Pi所指子树中所有结点的关键字均大于Ki,n(⌈m/2⌉-1<=n<=m-1)为关键字的个数(或n+1为子树个数)。(i=1,…,n)
B-树具有平衡、有序、多路的特点,一棵4阶的B-树:
(1)所有叶子结点均在同一层次,这体现出其平衡的特点。
(2)树中每个结点中的关键字都是有序的,且关键字Ki“左子树”中的关键字均小于Ki,而其“右子树”中的关键字均大于Ki,这体现出其有序的特点。
(3)除叶子结点外,有的结点中有一个关键字,两棵子树,有的结点中有两个关键字,三棵子树,这种4阶的B-树最多有三个关键字,四棵子树,这体现出其多路的特点。

2.B-树的查找
将给定值key与根结点的各个关键字K1,K2,…,Kj(1<=j<=m-1)进行比较,由于该关键字序列是有序的,所以查找时可以采用顺序查找,也可采用折半查找。查找时:
(1)若key=Ki(1<=i<=j),则查找成功;
(2)若key<K1,则顺着指针P0所指向的子树继续向下查找;
(3)若Ki<key<Ki+1(1<=i<=j-1),则顺着指针Pi所指向的子树继续向下查找;
(4)若key>Kj,则顺着指针Pj所指向的子树继续向下查找。
在含有N个关键字的B-树上进行查找时,从根节点到关键字所在结点的路径上涉及的结点数不超过log⌈m/2⌉ ((N+1)/2)+1。

3.B-树的插入
首先在最底层的某个非终端结点中添加一个关键字,若该结点的关键字个数不超过m-1(有空位置),则插入完成,否则表明结点已满,要产生结点的“分裂”,将此结点在同一层分成两个结点。一般情况下,结点分裂方法是:以中间关键字为界把结点一分为二,分成两个结点,并把中间关键字向上插入到双亲结点上,若双亲结点已满,则采用同样的方法继续分解。最坏的情况下,一直分解到树根结点,这时B-树高度增加1。

4.B-树的删除
m阶B-树的删除操作是在B-树的某个结点中删除指定的关键字及其邻近的一个指针,删除后应该进行调整使该树仍然满足B-树的定义,保证每个结点的关键字数目范围为[⌈m/2⌉-1,m]。删除记录后,结点的关键字个数如果小于m/2-1,则要进行“合并”结点的操作。若该结点为最下层的非终端结点,可直接删除;若该结点不是最下层的非终端结点,不可直接删除,可做如下处理:将要删除记录用其右(左)边邻近指针指向的子树中关键字最小(大)的记录(该记录必定在最下层的非终端结点中)替换。采用这种方法处理,删除记录都可归结为在最下层的非终端结点中删除记录的情况。
删除最下层非终端结点中的关键字的情形有以下3种可能:
(1)被删关键字所在结点中的关键字数目不小于⌈m/2⌉,则只需从该节点中删去该关键字Ki和相应指针Pi,树的其他部分不变。
(2)被删关键字所在结点中的关键字数目等于⌈m/2⌉-1,而与该节点相邻的右兄弟(或左兄弟)结点中的关键字数目大于⌈m/2⌉-1,则需将其兄弟结点中的最小(或最大)的关键字上移至双亲结点中,而将双亲结点中小于(或大于)且紧靠该上移关键字的关键字下移至被删关键字所在结点中。
(3)被删关键字所在结点和其相邻的兄弟结点中的关键字数目均等于⌈m/2⌉-1。假设该结点有右兄弟,且其右兄弟结点地址由双亲结点中的指针Pi所指,则在删去关键字之后,它所在结点中剩余的关键字和指针,加上双亲结点中的关键字Ki一起,合并到Pi所指兄弟结点中(若没有右结点,则合并至左兄弟结点中)。

7.3.4B+树
B+树是一种B-树的变形树,更适合用于文件索引系统。

1.B+树和B-树的差异
一棵m阶的B+树和m阶的B-树的差异在于:
(1)有n棵子树的结点中含有n个关键字;
(2)所有的叶子结点中包含了全部关键字的信息,以及指向含这些关键字记录的指针,且叶子结点本身依关键字的大小自小而大顺序链接;
(3)所有的非终端结点可以看成是索引部分,结点中仅含有其子树(根节点)中的最大(或最小)关键字。

2.B+树的查找、插入和删除
7.4散列表的查找(计算式查找法)
7.4.1散列表的基本概念
在元素的存储位置和其关键字之间建立某种直接关系,那么在进行查找时,就无需做比较或做很少次的比较,按照这种关系直接由关键字找到相应的记录。这就是散列查找法的思想,它通过对元素的关键字值进行某种运算,直接求出元素的地址,即使用关键字到地址的直接转换方法,而不需要反复比较。因此,散列查找法又叫杂凑法或散列法。
(1)散列函数和散列地址:在记录的存储位置p和其关键字key之间建立一个确定的对应关系H,使p=H(key),称这个对应关系H为散列函数,p为散列地址。
(2)散列表:一个有限连续的地址空间,用以存储按散列函数计算得到相应散列地址的数据记录。通常散列表的存储空间是一个一维数组,散列地址是数组的下标。
(3)冲突和同义词:对不同的关键字可能得到同一散列地址,即key1≠key2,而H(key1)=H(key2),这种现象称为冲突。具有相同函数值的关键字对该散列函数来说称作同义词,key1与key2互称为同义词。
7.4.2散列函数的构造方法
构造一个“好”的散列函数应遵循以下两条原则:(计算简单,分布均匀)
(1)函数计算要简单,每一关键字只能有一个散列地址与之对应;
(2)函数的值域需在表长的范围内,计算出的散列地址的分布应均匀,尽可能减少冲突。

1.数字分析法
如果事先知道关键字集合,且每个关键字的位数比散列表的地址码位数多,每个关键字由n位数组成,如k1k2…kn,则可以从关键字中提取数字分布比较均匀的若干位作为散列地址。

2.平方取中法
一个数平方后的中间几位数和数的每一位都相关,取关键字平方后的中间几位或其组合作为散列地址。

3.折叠法
将关键字分割成位数相同的几部分(最后一部分的位数可以不同),然后取这几部分的叠加和(舍去进位)作为散列地址,这种方法称为折叠法。根据位数叠加的方式,可以把折叠法分为移位叠加和边界叠加两种。移位叠加是将分割后每一部分的最低位对齐,然后相加;边界叠加是将两个相邻的部分沿边界来回折叠,然后对齐相加。

4.除留余数法
假设散列表表长为m,选择一个不大于m的数p,用p去除关键字,除后所得余数为散列地址,即H(key)=key%p,这个方法的关键是选取适当的p,一般情况下,可以选p为小于表长的最大质数。

5.伪随机数法:采用一个伪随机函数做哈希函数,即H(key)=random(key)。

7.4.3处理冲突的方法

1.开放地址法
基本思想:把记录都存储在散列表数组中,当某一记录关键字key的初始散列地址H0=H(key)发生冲突时,以H0为基础,采取合适方法计算得到另一个地址H1,如果H1仍然发生冲突,以H1为基础再求下一个地址H2,若H2仍然冲突,再求得H3。依此类推,直至Hk不发生冲突为止,则Hk为该记录在表中的散列地址。
通常把寻找“下一个”空位的过程称为探测,上述方法可用如下公式表示:Hi=(H(key)+di)%m i=1,2,…,k(k<=m-1),其中,H(key)为散列函数,m为散列表表长,di为增量序列。根据di取值的不同,可以分为以下3种探测方法:
(1)线性探测法
di=1,2,3,…,m-1
将散列表假想成一个循环表,发生冲突时,从冲突地址的下一单元顺序寻找空单元,如果到最后一个位置也没有找到空单元,则回到表头开始继续查找,直到找到一个空位。如果找不到空位,则说明散列表已满,需要进行溢出处理。
(2)二次探测法
di=12,-12,22,-22,32,…,+k2,-k2(k<=m/2)
(3)伪随机探测法
di=伪随机数序列
线性探测法处理冲突的过程中,在处理冲突过程中发生的两个第一个散列地址不同的记录争夺同一个后继散列地址的现象称作“二次聚集”(或称作“堆积”),即在处理同义词的冲突过程中又添加了非同义词的冲突。
线性探测法的优点是:只要散列表未填满,总能找到一个不发生冲突的地址。缺点是 :会产生“二次聚集”现象。而二次探测法和伪随机数法的优点是:可以避免“二次聚集”现象。缺点:不能保证一定找到不发生冲突的地址。

2.链地址法
基本思想:把具有相同散列地址的记录放在同一个单链表,称为同义词链表。有m个散列地址就有m个单链表,同时用数组HT[0…m-1]存放各个链表的头指针,凡是散列地址为i的记录都以结点方式插入到以HT[i]为头结点的单链表中。

3.再哈希法
同时构造多个不同的哈希函数Hi=RH1(key) i=1,2,…,k
当哈希地址Hi=RH1(key)发生冲突时,再计算Hi=RH2(key)…直到冲突不再产生;
这种方法不易产生聚集,但增加了计算时间。

4.建立公共溢出区
将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表。

7.4.4散列表的查找
(1)给定待查找的关键字key,根据造表时设定的散列函数计算H0=H(key)。
(2)若单元H0为空,则所查元素不存在。
(3)若单元H0中元素的关键字为key,则查找成功。
(4)否则重复下述解决冲突的过程:
按处理冲突的方法,计算下一个散列地址Hi;
若单元Hi为空,则所查元素不存在;
若单元Hi中 元素的关键字为key,则查找成功。

从散列表的查找过程可见:
(1)仍需以平均查找长度作为衡量散列表查找效率的量度。
(2)进行比较的个数取决于三个因素:散列函数、处理冲突的方法和散列表的装填因子。
散列表的装填因子α定义为α=表中填入的记录数/散列表的长度,α标志散列表的装满程度。α越小,发生冲突的可能性就越小;反之,α越大,表中已填入的记录越多,再填记录时发生冲突的可能性就越大。
链地址法的平均查找长度小于开放地址法。
在查找概率相等的前提下,直接计算查找成功的平均查找长度可以采用以下公式:ASLsucc=(1/n)∑ni=1Ci ,其中,n为散列表中记录的个数,Ci为成功查找第i个记录所需的比较次数。
而直接计算查找失败的平均查找长度可以采用以下公式:ASLunsucc=(1/r) ∑ri=1Ci ,其中,r为散列函数取值的个数,Ci为散列函数取值为i时查找失败的比较次数。

第8章排序

8.1基本概念和排序方法概述

8.1.1排序的基本概念

1.排序(基本操作:比较、移动)
排序是按关键字的非递减或非递增顺序对一组记录重新进行排序的操作。

2.排序的稳定性
当待排序的序列中存在两个或两个以上关键字相等的记录时,则排序所得结果不唯一。 假设Ki=Kj(i≠j),且在排序前的序列中Ri领先于Rj(即i<j)。若在排序后的序列中Ri仍领先于Rj,则称所用的排序方法是稳定的;反之,若可能使排序后的序列中Rj领先于Ri,则称所用的排序方法是不稳定的。
稳定性证明需要从排序的定义来证明;不稳定性的证明举出一组不满足稳定性的实例即可。

3.内部排序和外部排序
根据在排序过程中记录所占用的存储设备,可将排序分为两大类:一类是内部排序,指的是待排序记录全部存放在计算机内存中进行排序的过程;另一类是外部排序,在排序过程中尚需对外存进行访问的排序过程。

8.1.2内部排序的分类
插入类、交换类、选择类、归并类、分配类。

8.1.3待排序记录的存储方式
(1)顺序表
(2)链表
(3)待排序记录本身存储在一组地址连续的存储单元内,同时另设一个指示各个记录存储位置的地址向量,在排序过程中不移动记录本身,而移动地址向量中这些记录的“地址”,在排序结束后再按照地址向量中的值调整记录的存储位置。这种排序方式称为地址排序。

8.1.4排序算法效率的评价指标
(1)执行时间
时间主要消耗在关键字之间的比较和记录的移动上,排序算法的时间复杂度由这两个指标决定。
(2)辅助空间
空间复杂度由排序算法所需的辅助空间决定。

8.2插入排序
插入排序的基本思想:每一趟将一个待排序的记录,按其关键字的大小插入到已经排好序的一组记录的适当位置上,直到所有待排序记录全部插入为止。

8.2.1直接插入排序
直接插入排序是一种简单的排序方法,其基本操作是将一条记录插入到已经排好序的有序表中,从而得到一个新的、记录 数量增1的有序表。
在这里插入图片描述
(1)时间复杂度O(n2)。
(2)空间复杂度O(1)。
(3)算法特点:稳定排序;更适合于初始记录基本有序的情况。

8.2.2折半插入排序
用“折半查找”的方法查找当前记录在已排好序的序列中的插入位置。
(1)时间复杂度O(n2)。
(2)空间复杂度O(1)。
(3)算法特点:稳定排序;只能用于顺序结构,不能用于链式结构;更适合于无序、n较大的情况。

8.2.3希尔排序
希尔排序实质上是采用分组插入的方法。先将整个待排序记录序列分割成几组,从而减少参与直接插入排序的数据量,对每组分别进行直接插入排序,然后增加每组的数据量,重新分组。经过几次分组排序后,整个序列中的记录“基本有序”时,再对全体记录进行一次直接插入排序。
增量选取5、3和1:
在这里插入图片描述
(1)时间复杂度O(n1.3)。
(2)空间复杂度O(1)。
(3)算法特点:不稳定排序;只能用于顺序结构,不能用于链式结构;增量序列的值没有除1之外的公因子,最后一个增量值必须等于1;更适合于初始记录无序、n较大的情况。

8.3交换排序
交换排序的基本思想是:两两比较待排序记录的关键字,一旦发现两个记录不满足次序要求时则进行交换,直到整个序列全部满足要求为止。(交换逆序元素进行排序)

8.3.1冒泡排序
冒泡排序是一种最简单的交换排序方法,它通过两两比较相邻记录的关键字,如果发生逆序,则进行交换,从而使关键字小的记录如气泡一般逐渐往上“漂浮”(左移),或者使关键字大的记录如石块一样逐渐向下“坠落”(右移)。
在这里插入图片描述
(1)时间复杂度O(n2)。
(2)空间复杂度O(1)。
(3)算法特点:稳定排序;可用于链式存储结构;移动次数较多,算法平均时间性能比直接插入排序差;当初始记录无序、n较大时不宜采用。

8.3.2快速排序
快速排序:通过两个(不相邻)记录的一次交换,消除多个逆序。
在待排序的n个记录中任取一个记录(通常取第一个记录)作为枢轴,设其关键字为pivotkey。经过一趟排序后,把所有关键字小于pivotkey的记录交换到前面,把所有关键字大于pivotkey的记录交换到后面,结果将待排序记录分成两个子表,最后将枢轴放置在分界处的位置。然后,分别对左、右子表重复上述过程,直至每一子表只有一个记录时,排序完成。步骤如下:
(1)选择待排序表中的第一个记录作为枢轴,将枢轴记录暂存在r[0]的位置上。
(2)从表的最右侧位置依次向左搜索,找到第一个关键字小于枢轴关键字pivotkey的记录,将其移到low处。
(3)然后再从表的最左侧位置,依次向右搜索找到第一个关键字大于pivotkey的记录和枢轴记录交换。
(4)重复步骤(2)和(3),直至low和high相等为止。
在这里插入图片描述
(1)时间复杂度O(nlog2n)。
(2)空间复杂度最好O(log2n),最坏O(n)。
(3)算法特点:不稳定排序;适用于顺序结构,很难用于链式结构;更适合于初始记录无序、n较大的情况。

8.4选择排序
选择排序的基本思想是:每一趟从待排序的记录中选出关键字最小的记录,按顺序放在已排序的记录序列的最后,直到全部排完为止。

8.4.1简单选择排序
也称直接选择排序
在这里插入图片描述
(1)时间复杂度O(n2)。
(2)空间复杂度O(1)。
(3)算法特点:稳定排序,上图所表现出的不稳定现象是因为采用“交换记录”的策略所造成的;可用于链式结构;移动记录次数较少,当每一记录占用的空间较多时,此方法比直接插入排序快。

8.4.2树形选择排序
首先对n个记录的关键字进行两两比较,然后在其中⌈n/2⌉个较小者之间再进行两两比较,如此重复,直至选出最小关键字的记录为止。
时间复杂度为O(nlog2n),辅助空间较多,增加n-1个非叶子结点的辅助空间。

8.4.3堆排序
堆排序是一种树形选择排序,将待排序的记录r[1…n]看成是一棵完全二叉树的顺序存储结构。
堆的定义:n个元素的序列{k1,k2,…,kn}称之为堆,当且仅当满足以下条件时:(1)ki>=k2i且ki>=k2i+1或 (2)ki<=k2i且ki<=k2i+1 (1<=i<=⌊n/2⌋)
即:树中所有非终端结点的值均不大于(或不小于)其左、右孩子结点的值。 显然,在这两种堆中,堆顶元素(或完全二叉树的根)必为序列中n个元素的最大值(或最小值),分别称之为大根堆(或小根堆)。

用大根堆进行排序,堆排序的步骤如下:
(1)按堆的定义将待排序序列r[1…n]调整为大根堆(这个过程称为初建堆),交换r[1]和r[n],则r[n]为关键字最大的记录。
(2)将r[1…n-1]调整为大根堆,交换r[1]和r[n-1],则r[n-1]为关键字次大的记录。
(3)循环n-1次,直到交换了r[1]和r[2]为止,得到了一个非递减的有序序列r[1…n]。
1.调整堆
从r[2s]和r[2s+1]中选出关键字较大者,假设r[2s]的关键字较大,比较r[s]和r[2s]的关键字。
(1)若r[s].key>=r[2s].key,说明r[s]为根的子树已经是堆,不必做任何调整。
(2)若r[s].key<r[2s].key,交换r[s]和r[2s]。交换后,以r[2s+1]为根的子树仍是堆,如果以r[2s]为根的子树不是堆,则重复上述过程,将以r[2s]为根的子树调整为堆,直至进行到叶子结点为止。
2.初建堆
对于无序序列r[1…n],从i=n/2开始,反复调用筛选法调整堆,依次将以r[i],r[i-1],…,r[1]为根的子树调整为堆。
在这里插入图片描述
(1)时间复杂度O(nlog2n)。
(2)空间复杂度O(1)。
(3)算法特点:不稳定排序;只能用于顺序结构,不能用于链式结构;记录较少时不宜采用,当记录较多时较为高效。

8.5归并排序
归并排序就是将两个或两个以上的有序表合并成一个有序表的过程。将两个有序表合并成一个有序表的过程称为 2-路归并。
在这里插入图片描述
(1)时间复杂度O(nlog2n)。
(2)空间复杂度O(n)。
(3)算法特点:稳定排序;可用于链式结构,且不需要附加存储空间,但递归实现时仍需要开辟相应的递归工作栈。

8.6基数排序
基数排序是典型的分配类排序。分配类排序不需要比较关键字的大小,它是根据关键字中各位的值,通过对待排序记录进行若干趟“分配”与“收集”来实现排序的。

8.6.1多关键字的排序
(1)最高位优先法
(2)最低位优先法

8.6.2链式基数排序
存储结构:用链表存储待排序表
排序思想:多趟分配和收集完成排序(趟数为关键字位数)
假设记录的逻辑关键字由d个“关键字组成”,每个关键字可能取rd个值。只要从最低数位关键字起,按关键字的不同值将序列中记录“分配”到rd个队列中后再“收集”之,如此重复d次完成排序。按这种方法实现排序称之为基数排序。
(1)时间复杂度:每一趟分配的时间复杂度为O(n),每一趟收集的时间复杂度为O(rd),整个排序需进行d趟分配和收集,所以时间复杂度为O(d(n+rd))。
(2)空间复杂度O(n+rd)。
(3)算法特点:稳定结构;可用于链式结构,也可用于顺序结构;基数排序使用条件有严格的要求,需要知道各级关键字的主次关系和各级关键字的取值范围。

8.7外部排序

8.7.1外部排序的基本方法
在这里插入图片描述
8.7.2多路平衡归并的实现

8.7.3置换-选择排序

8.7.4最佳归并树
对长度不等的m个初始归并段,对k-路归并而言,若(m-1)MOD(k-1)=0,则不需加虚段,否则需附加 k-(m-1)MOD(k-1)-1个虚段,即第一次归并为(m-1)MOD(k-1)+1路归并。
8.8小结
在这里插入图片描述
直接插入排序、折半插入排序、冒泡排序和简单选择排序的速度较慢,速度较慢的算法实现过程比较简单,称之为简单的排序方法;
(1)当待排序的记录个数n较小时,可选用简单的排序方法。而当关键字基本有序时,可选用直接插入排序或冒泡排序,排序速度很快,其中直接插入排序最为简单常用、性能最佳。
(2)当n较大时,应该选用先进的排序方法。从平均时间性能而言,快速排序最佳。具体选用的原则是:

  • 当关键字分布随机,稳定性不做要求时,可采用快速排序;
  • 当关键字基本有序,稳定性不做要求时,可采用堆排序;
  • 当关键字基本有序,内存允许且要求排序稳定时,可采用归并排序;

(3)快速排序、堆排序和希尔排序都是不稳定的;
(4)若排序按记录的次关键字进行,则必须采用稳定的排序方法;
(5)若记录本身信息量较大,为避免移动记录耗费大量时间,可采用链式存储结构,比如直接插入排序、归并排序都易于在链表上实现。

  • 21
    点赞
  • 54
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值