数据结构(顺序表,栈,队列,串,查找,排序)

数据结构绪论
数据结构:逻辑结构,存储结构,数据的运算
1.数据的逻辑结构:是指数据元素之间的逻辑关系,即从逻辑关系上描述数据,它与数据的存储无关,是独立于计算机的。数据的逻辑结构分为线性结构和非线性结构,线性表是典型的线性结构,集合、树和图是典型的非线性结构,如图:

2.数据的存储结构:是指数据结构在计算机中的表示(又称映像),也称物理结构。它包括数据元素的表示和关系的表示。数据的存储结构是用计算机语言实现的逻辑结构,它依赖于计算机语言。数据的存储结构主要有顺序存储,链式存储,索引存储和散列存储。
顺序存储:把逻辑上相邻的元素存储在物理位置上也相邻的存储单元中,元素之间的关系由存储单元的邻接关系来体现。其优点是可以实现随机存取,每个元素占用最少的存储空间;缺点是只能使用相邻的一整块存储单元,因此可能产生较多的外部碎片。
链式存储:不要求逻辑上相邻的元素在物理位置上也相邻,借助指示元素存储地址的指针来表示元素之间的逻辑关系。其优点是不会出现碎片现象,能充分利用所有存储单元;缺点是每个元素因存储指针而占用额外的存储空间,且只能实现顺序存取。
索引存储:在存储元素信息的同时,还建立附加的索引表。索引表中的每项称为索引项,索引项一般形式是(关键字,地址)。其优点是检索速度快;缺点是附加的索引表额外占用存储空间。另外,增加和删除数据时也要修改索引表,因而会花费较多的时间。
散列存储:根据元素的关键字直接计算出该元素的存储地址,又称哈希存储。其优点是检索、增加和删除结点的操作都很快;缺点是若散列函数不好,则可能出现元素存储单元的冲突,而解决冲突会增加时间和空间开销。
3.数据的运算
施加在数据上的运算包括运算的定义和实现。运算的定义是针对逻辑结构的,指出运算的功能;运算的实现是针对存储结构的,指出运算的具体操作步骤。

常见的渐近时间复杂度
O(1)<O(log2n)<O(n)<O(nlog2n)<O(n2)<O(n3)<O(2n)<O(n!)<O(nn)

线性表
在这里插入图片描述
注意:线性表是一种逻辑结构,表示元素之间一对一的相邻关系。顺序表和链表是指存储结构,两者属于不同层面的概念,因此不要将其混淆。

顺序表:它是用一组地址连续的存储单元依次存储线性表中的数据元素,从而使得逻辑上相邻的两个元素在物理位置上也相邻。顺序表的特点是表中元素的逻辑顺序与其物理顺序相同。

//静态分配一维数组
#define MaxSize  50
typedef struct{
	ElemType data[MaxSize];
	int length;
}SqList;

//动态分配一维数组
#define InitSize 100
typedef struct{
	ElemType* data;
	int MaxSize,length;
}SeqList;
C的初始动态分配语句为
L.data = (ElemType *)malloc(sizeof(ElemType)*InitSize);
C++的初始动态分配语句
L.data = new ElemType[InitSize];

顺序表上基本操作实现
1.插入操作:在顺序表L的第i(1<=i<=length+1)个位置上插入新元素e。若i的输入不合法,则返回false,表示插入失败。

bool ListInsert(SqList &L,int i,ElemType e){
	if(i<1 || i>L.length+1) //判断i的范围是否有效
		return false;
	if(L.length>=MaxSize) //当前存储空间已满,不能插入
		return false;
	for(int j = L.length;j>=i;j--)
		L.data[j] = L.data[j-1];
	L.data[i-1] = e;
	L.length++;
	return true;
}

移动结点的平均次数为:结点概率*结点操作移动次数
在这里插入图片描述
因此,线性表插入算法的平均时间复杂度为O(n);
2.删除操作:删除顺序表L中第i(1<=i<=L.length)个位置的元素,用引用变量e返回。

bool ListDelete(SqList& L,int i,ElemType &e){
	if(i<1 || i>L.length) //判断i的范围是否有效
		return false;
	e = L.data[i-1];
	for(int j=i;j<L.length;j++)
		L.data[j-1] = L.data[j];
	L.length--;
	return true;
}

移动结点平均次数
在这里插入图片描述
因此,线性表删除算法的平均时间复杂度为O(n)。

线性表的链式表示
单链表:它是指通过一组任意的存储单元来存储线性表中的数据元素。为了建立数据元素之间的线性关系,对每个链表结点,除存放元素自身的信息外,还需要存放一个指向其后继的指针。
单链表中结点类型的描述如下:

typedef struct LNode{
	ElemType data;
	struct LNode* next;
}LNode,*LinkList;

由于单链表的元素离散地分布在存储空间中,所以单链表是非随机存取的存储结构,即不能直接找到表中某个特定的结点。查找某个特定的结点时,需要从表头开始遍历,依次查找。
注意:通常用头指针来标识一个单链表,如单链表L,头指针为NULL时表示一个空表。此外,为了操作上的方便,在单链表第一个结点之前附加一个结点,称为头结点。头结点的数据域可以不设任何信息,也可以记录表长等信息。头结点的指针域指向线性表的第一个元素结点。
在这里插入图片描述
头结点和头指针的区分:不管带不带头结点,头指针都始终指向链表的第一个结点。,而头结点是带头结点的链表中的第一个结点,结点内通常不存储信息。
引入头结点后,可以带来两个优点:
1、由于第一个数据结点的位置被存放在头结点的指针域中,因此在链表的第一个位置上的操作和在表的其他位置上的操作一致,无须进行特殊处理
2、无论链表是否为空,其头指针都指向头结点的非空指针(空表中头结点的指针域为空),因此空表和非空表的处理也得到了统一。

单链表基本操作实现
1、采用头插法建立单链表:该方法从一个空表开始,生成新结点,并将读取到的数据存放到新结点的数据域中,然后将新结点插入到当前链表的表头,即头结点之后,
在这里插入图片描述

LinkList List_HeadInsert(LinkList& L){
	LNode *s;
	int x;
	L = (LinkList)malloc(sizeof(LNode));
	L->next = NULL;
	cin>>s;
	while(x!=9999){
		s = (LNode*)malloc(sizeof(LNode));
		s->data = x;
		s->next = L->next;
		L->next = s;
		cin>>x;
	}
	return L;
}

采用头插法建立单链表时,读入数据的顺序与生成的链表中的元素顺序是相反的。每个结点插入的时间为O(1),设单链表长为n,则总时间复杂度为O(n)。

2、采用尾插法建立单链表:头插法建立单链表的算法虽然简单,但生成的链表结点的次序和输入数据的顺序不一致。若希望两者次序一致,则可采用尾插法。该方法将新结点插入到当前链表的表尾,为此必须增加一个尾指针r,使其始终指向当前链表的尾结点。
在这里插入图片描述

LinkList List_TailInsert(LinkList& L){
	int x;
	L = (LinkList)malloc(sizeof(LNode));
	LNode *s,*r = L;
	cin>>x;
	while(x!=9999){
		s = (LNode*)malloc(sizeof(LNode));
		s->data = x;
		r->next = s;
		r = s;
		cin>>s;
	}
	r->next = NULL;
	return L;
}

3、按序号查找结点值:从第一个结点出发,顺指针next域逐个往下搜索,直到找到第i个结点为止,否则返回最一个结点指针域NULL

LNode* GetElem(LinkList L,int i){
	int j = 1;
	LNode* p = L->next;
	if(i == 0) return L;
	if(i<1) return NULL;
	while(p && j<i){
		p = p->next;
		j++;
	} 
	return p;
}

按序号查找操作的时间复杂度为O(n)。

4、插入结点操作:插入结点操作将值为x的新结点插入到单链表的第i位置上。先检查插入位置的合法性,然后找到待插入位置的前驱结点,即第i-1个结点,再在其后面插入新结点。算法首先调用按序号查找算法GetElem(L,i-1),查找第i-1个结点。假设返回的第i-1个结点为p,然后令新结点s的指针域指向p的后继结点,再令结点p的指针域指向新插入的结点*s。
在这里插入图片描述

p = GetElem(L,i-1);
s->next = p->next;
p->next = s;

本算法主要的时间开销在于查找第i-1个元素,时间复杂度为O(n),若在给定的结点后面插入新结点,则时间复杂度为O(1)。
此外,可采用另一种方式将其转化为后插操作来实现,设待插入结点为s,将s插入到p的前面,我们仍然将s插入到*p的后面,然后将p->data与s->data交换,这样既满足了逻辑关系,又能使得时间复杂度为O(1)。

s->next = p->next;
p->next = s;
temp = s->data;
s->data = p->data;
p->data = temp;

5.删除结点:删除结点操作是将单链表的第i个结点删除。先检查删除位置的合法性,后查找表中第i-1个结点,即被删结点的前驱结点,再将其删除。假设结点p为找到的被删结点的前驱结点,仅需修改p的指针域,即将p的指针域next指向q的下一结点。
在这里插入图片描述

p = GetElem(L,i-1);
q = p->next;
p->next = q->next;
free(q);

该算法的主要时间也耗费在查找操作上,时间复杂度为O(n)。
其实,删除结点p的操作可用删除p的后继结点操作来实现,实质就是将其后继结点的值赋予其自身,然后删除后继结点,也能使时间复杂度为O(1)。

q = p->next;
p->data = q->next->data;
p->next = q->next;
free(q);

双链表
单链表要访问某个结点的前驱结点(插入、删除时),只能从头开始遍历,访问后继结点的时间复杂度为O(1),访问前驱结点的时间复杂度为O(1)。
为了克服单链表的上述缺点,引入双向链表,双向链表结点中有两个指针prior和next,分别指向其前驱结点和后继结点。
在这里插入图片描述
双链表中结点类型的描述如下:

typedef struct DNode{
	ElemType data;
	struct DNode* prior,next;
}DNode,*DLinkList;

1.双链表的插入操作:在双链表中p所指向的结点之后插入结点*s,其指针变化过程如下:
在这里插入图片描述
插入操作的代码片段如下:

s->next = p->next;
p->next->prior = s;
p->next = s;
s->prior = p;

2.双链表的删除操作:删除双链表中结点p的后继结点q,其指针变化过程如下:
在这里插入图片描述

p->next = q->next;
q->next->prior = p;
free(q);

循环链表
1.循环单链表:循环单链表和单链表的区别在于,表中最后一个结点的指针不是NULL,而改为指向头结点,从而整个链表形成一个环。
在循环单链表中,表尾结点*r的next域指向L,故表中没有指针域为NULL的结点,因此循环单链表的判空条件不是头结点的指针是否为空,而是它是否等于头指针。
在这里插入图片描述
循环单链表的插入和删除算法与单链表几乎一样,所不同的是若操作是在表尾进行,则执行的操作不同,以让单链表继续保持循环的性质。而循环单链表在任何一个位置上的插入和删除操作都是等价的,无须判断是否是表尾。
注意:对循环单链表不设头指针而仅设尾指针,从而使操作效率更高。其原因是,若设的是头指针,对表尾进行操作需要O(n)的时间复杂度,而若设的是尾指针r,r->next即为头指针,对表头与表尾的操作都只需要O(1)的时间复杂度。

2.循环双链表:需要注意的是,头结点的prior指针还要指向表尾结点。
在这里插入图片描述
在循环双链表L中,某结点*p为尾结点时,p->next == L;
当循环双链表为空表时,其头结点的prior域和next域都等于L。

静态链表:静态链表借助数组来描述线性表的链式存储结构。结点也有数据域data和指针域next,与前面所讲的链表中的指针不同的是,这里的指针是结点的相对地址(数组下标),和顺序表一样,静态链表也要预先分配一块连续的内存空间。
在这里插入图片描述
静态链表结构类型的描述如下:

#define MaxSize 50
typedef struct{
	ElemType data;
	int next;
}SLinkList[MaxSize];

静态链表以next == -1作为其结束的标志。静态链表的插入和删除操作与动态链表的相同,只需要修改指针,而不需要移动元素。

栈和队列
在这里插入图片描述
栈:只允许在一端进行插入或删除操作的线性表。首先栈是一种线性表,但限定这种线性表只能在某一端进行插入和删除操作。
在这里插入图片描述
栈顶:线性表允许进行插入删除的那一端
栈底:固定的,不允许进行插入和删除的另一端
空栈:不含任何元素的空表
栈的数学性质:n个不同元素进栈,出栈元素不同排列的个数为C(n,2n)/(n+1)。上述公式称为卡特兰数,可采用数学归纳法证明。

顺序栈:采用顺序存储的栈称为顺序栈,它利用一组地址连续的存储单元存放自栈底到栈顶的数据元素,同时附设一个指针(top)指示当前栈顶元素的位置。
栈的顺序存储类型可描述为:

#define MaxSize 50
typedef struct{
	ElemType data[MaxSize];
	int top;
}SqStack;

栈顶指针:S.top,初始时设置S.top = -1;
栈顶元素:S.data[top];
进栈操作:栈不满时,栈顶指针先加1,再送值到栈顶元素。
出栈操作:栈非空时,先取栈顶元素值,再将栈顶指针减1。
栈空条件:S.top == -1;
栈满条件:S.top == MaxSize - 1;
栈长:S.top+1;

顺序栈的基本运算
在这里插入图片描述

//初始化
void InitStack(SqStack& s){
	s.top = -1;
}
//判栈空
bool StackEmpty(SqStack s){
	if(s.top == -1)
		return true;
	else return false;
}
//进栈
bool push(SqStack& s){
	if(s.top == MaxSize-1)
		return false;
	s.data[++s.top] = x;
	return true;
}
//出栈
bool pop(SqStack& s){
	if(s.top == -1) return false;
	x = s.data[s.top--];
	return true;
}
//读栈顶元素
bool GetTop(SqStack s,ElemType& x){
	if(s.top == -1)
		return false;
	x = s.data[s.top];
	return true;
}

共享栈:利用栈底位置相对不变的特性,可让两个顺序栈共享一个一维数组空间,将两个栈的栈底分别设置在共享空间的两端,两个栈顶向共享空间的中间延伸。
在这里插入图片描述
两个栈的栈顶指针都指向栈顶元素,top0 == -1时0号栈为空。top1 == MaxSize时1号栈为空。仅当两个栈顶指针相邻时(top1 - top0 == 1)时,判断为栈满。当0号栈进栈时top0先加1再赋值,1号栈进栈时top1先减1再赋值。出栈时则刚好相反。

栈的链式存储结构:采用链式存储的栈称为链栈,链栈的优点是便于多个栈共享存储空间和提高其效率,且不存在栈满上溢的情况。通常采用单链表实现,并规定所有操作都是在单链表的表头进行的。这里规定栈没有头结点,Lhead指向栈顶元素。
在这里插入图片描述
栈的链式存储可描述边:

typedef struct Linknode{
	ElemType data;
	struct Linknode* next;
}*LiStack;

队列
队列:是一种操作受限的线性表,只允许在表的一端进入插入,而在表的另一端进行删除。向队列中插入元素称为入队或进队,删除元素称为出队或离队。其操作特性就是先进先出。
在这里插入图片描述
队头:允许删除的一端。
队尾:允许插入的一端。
空队列:不含任何元素的空表

1.队列的顺序存储:是指分配一块连续的存储单元存放队列中的元素,并附设两个指针:队头指针front指向队头元素,队尾指针rear指向队尾元素的下一个位置。
队列的顺序存储类型可描述为:

#define MaxSize 50
typedef struct{
	ElemType data[Maxsize];
	int front,rear;
}SqQueue;

初始状态(队空条件):Q.front == Q.rear == 0
进队操作:队不满时,先送值到队尾元素,再将队尾指针加1
出队操作:队不空时,先取队头元素值,再将队头指针加1
注意:不能用Q.rear == MaxSize 作为队列满的条件,如图中的d中,队列中仅有一个元素,但仍满足该条件。这里入队出现“上溢出”,但这种溢出并不是真正的溢出,在data数组中依然存在可以存放元素的空位置。所以是一种“假溢出”。
在这里插入图片描述

循环队列:前面指出顺序队列的缺点,这里引出循环队列的概念。将顺序队列臆造为一个环状的空间,即把存储队列元素的表从逻辑上视为一个环,称为循环队列。当队首指针Q.fornt ==MaxSize-1后,再前进一个位置就自动到0,这可以利用除法取余运算来实现
初始时:Q.front == Q.rear == 0
队首指针进1:Q.front = (Q.front+1)%MaxSize;
队尾指针进1:Q.rear = (Q.rear+1)%MaxSize;
队列长度:(Q.rear-Q.front+MaxSize)%MaxSize;
注意:循环队列队空和队满的判断条件不能是Q.front == Q.rear。因为若入队元素的速度快于出队元素的速度,则队尾指针很快就会赶上队首指针,此时可以看出队满时也有Q.front == Q.rear。
在这里插入图片描述
为了区分是队空还是队满的情况,有三种处理方式:
1.牺牲一个单元来区分队空和队满,入队时少用一个队列单元,约定以“队头指针在队尾指针的下一位置作为队满的标志”
队满条件:(Q.rear+1)%MaxSize == Q.front;
队空条件仍:Q.front == Q.rear;
队列中元素的个数:(Q.rear - Q.front+MaxSize)%MaxSize.
2.类型中增设表示元素个数的数据成员。这样,队空的条件为Q.szie == 0;队满的条件为Q.size == Maxsize。这两种情况都有Q.front == Q.rear。
3.类型中增设tag数据成员,以区分是队满还是队空。tag等于0时,若因删除导致Q.front == Q.rear,则为队空。tag等于1时,若因插入导致Q.front == Q.rear。则为队满

循环队列的操作

//初始化
void InitQueue(SqQueue& q){
	q.front = q.rear = 0;
}
//判队空
bool isEmpty(SqQueue q){
	if(q.front == q.rear)
		return true;
	else return false;
}
//入队
bool EnQueue(SqQueue& q,ElemType x){
	if((q.rear+1)%MaxSize == q.front) return false;
	q.data[q.rear] = x;
	q.rear = (q.rear+1)%MaxSize;
	return true;
}
//出队
bool DeQueue(SqQueue& q,ElemType& x){
	if(q.rear == q.front) return false;
	x = q.data[q.front];
	q.front = (q.front+1)%MaxSize;
	return true;
}

队列的链式存储:队列的链式表示称为链队列,它实际上是一个同时带有队头指针和队尾指针的单链表。头指针指向队头结点,尾指针指向队尾结点,即单链表的最后一个结点。
在这里插入图片描述
队列的链式存储类型可描述为

typedef struct{
	ElemType data;
	struct LinkNode * next;
}LinkNode;
typedef struct{
	LinkNode* front,rear;
}LinkQueue;

队空:Q.front == Q.rear == NULL。
出队:首先判断队是否为空,若不空,则取出队头元素,将其从链表中摘除,并让Q.front指向下一个结点(若该结点为最后一个结点,则置Q.front和Q.rear为NULL)。
入队:建立一个新结点,将新结点插入链表的尾部,并改让Q.rear指向这个新插入的结点(若原队列为空队,则令Q.front也指向该结点 )
由于不带头结点的链式队列在操作上往往比较麻烦,因此通常将链式队列设计成一个带头结点的单链表,这样插入和删除操作就统一了。在这里插入图片描述
链式队列的基本操作

//初始化
void InitQueue(LinkQueue& q){
	q.front = q.rear = (LinkNode*)malloc(sizeof(LinkNode));
	q.front->next = NULL;
}
//判队空
bool isEmpty(LinkQueue q){
	if(q.front == q.rear) return true;
	else return false;
}
//入队
void EnQueue(LinkQueue& q,ElemType x){
	LinkNode* s = (LinkNode*)malloc(sizeof(LinkNode));
	s->data = x;
	s->next = NULL;
	q.rear->next = s;
	q.rear = s;
}
//出队
bool DeQueue(LinkQueue& q,ElmeType& x){
	if(q.front == q.rear) return false;
	LinkNode* p = q.front->next;
	x = p->data;
	q.front->next = p->next;
	if(p == q.rear)
		q.rear = q.front;
	free(p);
	return true;
	
}

双端队列:是指允许两端都可以进行入队和出队的操作的队列。其元素的逻辑结构仍是线性结构。将队列的两端分别称为前端和后端,两端都可以入队和出队。
在这里插入图片描述

栈和队列的应用
栈的应用
1.栈在括号匹配中的应用
2.栈在表达式求值中的应用
3.栈在递归中的应用
队列的应用
1.队列在层次遍历中的应用


在这里插入图片描述
定长顺序存储表示:类似于线性表的顺序存储结构,用一组地址连续的存储单元存储串值的字符序列。在串的定长顺序存储结构中,为每个串变量分配一个固定长度的存储区,即定长数组。

#define MAXLEN 225
typedef struct {
	char ch[MAXLEN];
	int length;
}SString;

串的实际长度只能小于等于MAXLEN,超过预定义长度的串值会被舍去,称为截断。串长有两种表示方法:一是如上述定义描述的那样,用一个额外的变量len来存放串的长度;二是在串值后面加一个不计入串长的结束标记字符‘\0’,此时的串长为隐含值。
在一些串的操作(如插入、联接等)中,若串值序列的长度超过上界MAXLEN,约定用“截断”法处理,要克服这种弊端,只能不限定串长的最大长度,即采用动态分配的方式。

堆分配存储表示:堆分配存储表示仍然以一组地址连续的存储单元存放串值的字符序列,但它们的存储空间是在程序执行过程中动态分配得到的。

typedef struct{
	char *ch;
	int length;
}HString;

在C语言中,存在一个称之为“堆”的自由存储区,并用malloc和free函数来完成动态存储管理。利用malloc为每个新产生的串分配一块实际串长所需的存储空间,若分配成功,则返回一个指向起始地址的指针,作为串的基地址,这个串由ch指针来指示。若分配失败,则返回NULL。已分配的空间可用free释放掉。

块链存储表示:类似于线性表的链式存储结构,也可采用链表方式存储串值。由于串的特殊性(每个元素只有一个字符),在具体实现时,每个结点即可以存放一个字符,也可以存放多个字符。每个结点称为块,整个链表称为块链结构。如图是结点大小为4(即每个结点存放4个字符)的链表,最后一个结点占不满时通常用“#”补上。图二是结点大小为1的链表
在这里插入图片描述

串的模式匹配

查找
在这里插入图片描述
平均查找长度:在查找过程中,一次查找的长度是指需要比较的关键字次数,而平均查找长度则是所有查找过程中进行关键字的比较次数的平均值,其数学定义为
在这里插入图片描述
n是查找表的长度,Pi是查找第i数据元素的概率,一般认为每个数据元素的查找概率相等,即Pi = 1/n;Ci是找到第i个数据元素所需要进行的比较次数。平均查找长度是衡量查找算法效率的最主要的指标。

顺序查找
一般线性表的顺序查找:其基本思想是从线性表的一端开始,逐个检查关键字是否满足给定的条件。若查找到某个元素的关键字满足给定条件,则查找成功,返回该元素在线性表中的位置;若已经查找到表的另一端,但还没有查找到符合给定条件的元素,则返回查找失败的信息。下面给出其算法,主要是为了说明其中引入“哨兵”的作用。

typedef struct{
	ElemType *elem;//元素存储空间基址,建表时按实际长度分配,0号单元留空
	int TableLen;
}SSTable
int Search_Seq(SSTable ST,ElemTye key){
	ST.elem[0] = key;
	for(int i = ST.TableLen ; ST.elem[i]!=key;i--);
	return i;//若表中不存在关键字为key的元素,将查找到i为0时退出for循环
}

在上述算法中,将ST.elem[0]称为“哨兵”。引入它的目的是使得Search_Seq内的循环不必判断数组是否越界,因为满足i==0时,循环一定会跳出。需要说明的是,在程序中引入“哨兵”并不是这个算法独有的。引入“哨兵”可以避免很多不必要的判断语句,从而提高程序效率。
对于有n个元素的表,给定值key与表中第i个元素相等,即定位第i个元素时,需进行n-i+1次关键字的比较,即Ci=n-i+1。查找成功时,顺序查找的平均长度为
ASL(成功) = (1~n)Pi(n-i+1) = (n+1)/2
查找不成功时,与表中各关键字的比较次数显然是n+1次,从而顺序查找不成功的平均查找长度为ASL(不成功)=n+1。
注意:对线性的的链表只能进行顺序查找。

有序表的顺序查找:假设表L是按关键字从小到大排序的,查找的顺序是从前往后,待查找元素的关键字为key,当查找到第i个元素时,发现第i个元素对应的关键字小于key,但第i+1个元素对应的关键字大于key,这时就可以返回查找失败的信息,因为第i个元素之后的元素的关键字均大于key,所以表中不存在关键为key的元素。
在这里插入图片描述
上图的判定树来描述有序线性表的查找过程。树中的圆形结点表示有序线性表中存在的元素;树中的矩形结点称为失败结点(若有n个结点,则相应地有n+1个查找失败结点),它描述的是那些不在表中的数据值的集合。若查找到失败结点,则说明查找不成功。
在有序线性表的顺序查找中,查找成功的平均查找长度和一般线性表的顺序查找一样。查找失败时,查找指针一定走到了某个失败结点。这些失败结点是我们虚构的空结点。实际上是不存在的,所以到达失败结点时所查找的长度等于它上面的一个圆形结点所在层数。查找不成功的平均查找长度在相等查找概率的情形下:
ASL(不成功) = (1~n)Qj(Lj-1) = (1+2+…+n+n)/(n+1) = n/2 + n/(n+1)
式中,Qj是到达第j个失败结点的概率,在相等查找概率的情形下,它为1/(n+1);Lj是第j个失败结点所在的层数。

折半查找(二分查找):它仅适用于有序的顺序表。
其基本思想:首先将给定值key与表中中间位置的元素比较,若相等,则查找成功,返回该元素的存储位置;若不等,则所需查找的元素只能在中间元素以外的前半部分或后半部分(例如,在查找表升序排列时,若给定值key大于中间元素,则所查找的元素只可能在后半部分)。然后在缩小的范围内继续进行同样的查找,如此重复,直到找到为止,或确定表中没有所需要查找的元素,则查找不成功,返回查找失败的信息。

int Binary_Search(SeqList L, ElemType key){
	int l = 0;
	int r = L.TableLen-1;
	int mid;
	while(l<=r){
		mid = (l+r)/2;
		if(L.elem[mid] == key) return mid;
		else if(L.elem[mid] > key) r = mid-1;
		else l = mid+1}
	return -1;
}

在这里插入图片描述

折半查找的过程可用如图的二叉树来描述,称为判定树。树中每个圆形结点表示一个记录,结点中的值为该记录的关键字值;树中最下面的叶结点都是方形的,它表示查找不成功的情况。
在这里插入图片描述

从判定树可以看出,查找成功时的查找长度为从根结点到目的结点的路径上的结点数,而查找不成功时的查找长度为从根结点到对应失败结点的父结点的路径上的结点数;每个结点值均大于其左子结点值,且均小于其右子结点值。若有序序列有n个元素,则对应的判定树有n个圆形的非叶结点和n+1个方形的叶结点,显然,判定树是一棵平衡二叉树。
由上述分析,用折半查找法查找到给定值的比较次数最多不会超过树的高度,在等概率查找时,查找成功的平均查找长度为:
ASL(成功) = (1~n)Li (1/n) = (1/n)(11+22+…+h2(h-1)) = log(n+1)(n+1)/n - 1 = log(n+1) -1
式中,h是树的高度,并且元素个数为n时树高 h = log(n+1)上取整。所以,折半查找的时间复杂度为O(logn)。

分块查找:将查找表分为若干子块。块内的元素可以无序,但块之间是无序的,即第一个块中的最大关键字小于第二个块中的所有记录的关键字,第二个块中的最大关键字小于第三个块中的所有记录的关键字,以此类推。再建立一个索引表,索引表中的每个元素含有各块的最大关键字和各块中的第一个元素的地址。
分块查找的过程分为两步:第一步是在索引表中确定待查记录所在的块,可以顺序查找或折半查找索引表;第二步是在块内顺序查找。
在这里插入图片描述

B树
B树,又称多路平衡查找树,B树中所有结点的孩子个数的最大值称为B树的阶,通常用m表示,一棵m阶B树或为空树,或为满足如下特性的m叉树:
1.树中每个结点至多有m棵子树,即至多含有m-1个关键字。
2.若根结点不是终端结点,则至少有两棵子树。
3.除根结点外的所有非叶结点至少有m/2(上取整)棵子树,即至少含有m/2(上取整)-1个关键字。
4.所有非叶结点的结构如下:
在这里插入图片描述
其中,Ki(i=1,2,3…,n)为结点的关键字,且满足K1<K2<…<Kn;
Pi(i=1,2,3…,n)为指向子树根结点的指针,且指针Pi-1所指子树中所有结点的关键字均小于Ki,Pi所指子树中所有结点的关键字均大于Ki,n(m/2(上取整)-1 <= n <= m-1)为结点中关键字个数。
5.所有的叶结点都出现在同一层次,并且不带信息。
如图的B树中所有结点的最大孩子数m=5,因此它是一棵5阶B树,在m阶B树中结点最多可以有m个孩子。
在这里插入图片描述
1.如果根结点没有关键字就没有子树,此时B树为空;如果根结点有关键字,则其子树必然大于等于两棵,因为子树个数等于关键字个数加1
2.结点中关键字从左到右递增有序,关键字两侧均有指向子树的指针,左边指针所指子树的所有关键字均小于该关键字,右边指针所指子树的所有关键字均大于该关键字。或者看成下层结点关键字总是落在由上层结点关键字所划分的区间内。
3.所有叶结点均在第4层,代表查找失败的位置。

若n>=1,则对任意一棵包含n个关键字、高度为h、阶数为m的B树。
1.因为B树中每个结点最多有m棵子树,m-1个关键字,所以在一棵高度为h的m阶B树中关键字的个数应满足n<=(m-1)(1+m+m2+…+mh-1) = mh-1,因此有 h>=logm(n+1)
2.若让每个结点中的关键字个数达到最少,则容纳同样多关键字的B树的高度达到最大。由B树的定义:第一层至少有1个结点,第二层至少有2个结点。除根结点外的每个非终端结点至少有(m/2上取整)棵子树,则第三层至少有2(m/2上取整)个结点…第h+1层至少有2(m/2上取整)h-1个结点,注意到第h+1层是不包含任何信息的叶结点。对于关键字个数为n的B树,叶结点即查找不成功的结点为n+1,由此有n+1 >= 2(m/2上取整)h-1,即 h<= log(m/2上取整)((n+1)/2)+1
即最后 logm(n+1) <= h <= log(m/2上取整)((n+1)/2)+1

B树的查找:B树的查找包含两个基本操作:1.在B树中找结点;2.在结点内找关键字。
例如,图7.4中查找关键字42,首先从根结点开始,根结点只有一个关键字,且42>22,若存在,必在关键字22的右边子树上,右孩子结点有两个关键字,而36<42<45,则若存在,必在36和45中间的子树上,在该子结点中查到关键字42,查找成功。查找到叶结点时(对应指针为空指针),则说明树中没有对应的关键字,查找失败。

B树的插入:在B树中找到插入的位置后,并不能简单地将其添加到终端结点中,因为此时可能会导致整棵树不再满足B树定义的要求。
1.定位:利用前述的B树查找算法,找出插入该关键字的最低层中的某个非叶结点(在B树中查找key时,会找到表示查找失败的叶结点,这样就确定了最底层非叶结点的插入位置。注意:插入位置一定是最低层中的某个非叶结点)。
2.插入:在B树中,每个非失败结点的关键字个数都在区间 [ (m/2上取整)-1,m-1 ]内。插入后的结点关键字个数个于m,可以直接插入;插入后检查被插入结点内关键字的个数,当插入后的结点关键字个数大于m-1时,必须对结点进行分裂。

分裂方法:取一个新结点,在插入key后的原结点,从中间位置(m/2上取整)将其中的关键字分为两部分,左部分包含的关键字放在原结点中,右部分包含的关键字放到到新结点中,中间位置(m/2上取整)的结点插入原结点的父结点。若此时导致其父结点的关键字个数也超过了上限,则继续进行这种分裂操作,直至这个过程传到根结点为止,进而导致B树高度增1。

对于m=3的B树,所有结点中最多有m-1=2个关键字,若某结点中已有两个关键字,则结点已满,如图所示,插入一个关键字60后,结点内的关键字个数超过了m-1,此时必须进行结点分裂。
在这里插入图片描述

B树的删除:删除比较复杂,即要使得删除后的结点中的关键字个数>=(m/2上取整)-1,因此将涉及结点的”合并“问题。

当被删关键字k不在终端结点(最低层非叶结占)中时,可以用k的前驱(或后继)k来替代k,然后在相应的结点中删除k,关键字k必定落在某个终端结点中,则转换成了被删关键字在终端结点中的情形。如图4阶B树中,删除关键字80,用其前驱78代替,然后在终端结点中删除78。因此只需要讨论删除终端结点中关键字的情形。
在这里插入图片描述
当被删关键字在终端结点(最低层非叶结点)中时,有下列三种情况:
1.直接删除关键字。若被删除关键字所在结点的关键字个数>=(m/2上取整),表明删除该关键字后仍满足B树的定义,则直接删去该关键字。
2.兄弟够借。若被删除关键字所在结点删除前的关键字个数=(m/2上取整)-1,且与此结点相邻的右(或左)兄弟结点的关键字个数>=(m/2上取整),则需要调整该结点、右(或左)兄弟结点及其双亲结点(父子换位法),以达到新的平衡。如图中删除4阶B树的关键字65,右兄弟关键字个数>=(m/2上取整) = 2,将71取代原65的位置,将74调整到71的位置。
在这里插入图片描述
3.兄弟不够借。若被删除关键字所在结点删除前的关键字个数=(m/2上取整)-1,且此时与该结点相邻的左、右兄弟结点的关键字个数均=(m/2上取整)-1,则将关键字删除后与左(或右)兄弟结点及双亲结点中的关键字进行合并。在图中删除4阶B树的关键字5,它及其右兄弟结点的关键字个数=(m/2上取整)-1 = 1,故在5删除后将60合并到65结点中。

在合并过程中,双亲结点中的关键字个数会减1。若其双亲结点是根结点且关键字个数减少到0(根结点关键字个数为1时,有2棵子树),则直接将根结点删除,合并后的新结点成为根;若双亲结点不是根结点,且关键字个数减少到(m/2上取整)-2,则又要与它自己的兄弟结点进行调整或合并操作,并重复上述步骤,直至符合B树的要求。

B+树的基本概念
一棵m阶的B+树需满足下列条件:
1.每个分支结点最多有m棵子树(孩子结点)。
2.非叶根结点至少有两棵子树,其他每个分支结点至少有(m/2上取整)棵子树。
3.结点的子树个数与关键字个数相等。
4.所有叶结点包含全部关键字及指向相应记录的指针,叶结点中将关键字按大小顺序排列,并且相邻叶结点按大小顺序相互链接起来。
5.所有分支结点(可视为索引的索引)中仅包含它的各个子结点(即下一级的索引块)中关键字的最大值及指向其子结点的指针。

m阶的B+树和m阶的B树的主要差异如下:
1.在B+树中,具有n个关键字的结点只含有n棵子树,即每个关键字对应一棵子树;而在B树中,具有n个关键字的结点含有n+1棵子树
2.在B+树中,每个结点(非根内部结点)的关键字个数n的范围是 (m/2上取整)<=n<=m(根结点:1<=n<=m);在B树中,每个结点(非根内部结点)的关键字个数n的范围是 (m/2上取整)-1<=n<=m-1(根结点:1<=n<=m-1)
3.在B+树中,叶结点包含信息,所有非叶结点仅起索引作用,非叶结点中的每个索引项只含有对应子树的最大关键字和指向该子树的指针,不含有该关键字对应记录的存储地址。
4.在B+树中,叶结点包含了全部关键字,即在非叶结点中出现的关键字也会出现在叶结点中;而在B树中,叶结点(最外层内部结点)包含的关键字和其他结点包含的关键字是不重复的。
在这里插入图片描述
如图可以看出:分支结点的某个关键字是其子树中最大关键字的副本。通常在B+树中有两个头指针;一个指向根结点,另一个指向关键字最小的叶结点。因此,可以对B+树进行两种查找运算:一种是从最小关键字开始的顺序查找,另一种是从根结点开始的多路查找。
注意:B+树的查找、插入和删除操作和B树的基本类似。只是在查找过程中,非叶结点上的关键字值等于给定值时并不终止,而是继续向下查找,直到叶结点上的该关键字为止。所以,在B+树中查找时,无论查找成功与否,每次查找都是一条从根结点到叶结点的路径。

散列表
常用的散列函数
1.直接定址法:直接取关键字的某个线性函数值为散列地址,散列函数为:
H(key) = key 或 H(key) = a*key+b
式中,a和b是常数。这种方法计算最简单,且不会产生冲突。它适合关键字的分布基本连续的情况,若关键字分布不连续,空位较多,则会造成存储空间的浪费。

2.除留余数法:假定散列表表长为m,取一个不大于m但最接近或等于m的质数p,利用以下公式把关键字转换成散列地址。散列函数为
H(key) = key%p
除留余数法的关键是选好p,使得每个关键字通过该函数转换后等概率地映射到散列空间上的任一地址,从而尽可能减少冲突的可能性。
3.数字分析法
4.平方取中法

处理冲突的方法:任何设计出来的散列函数都不可能绝对地避免冲突。为些,必须考虑在发生冲突时应该如何处理,即为产生冲突的关键字寻找下一个”空“的Hash地址。用Hi表示处理冲突中第i次探测得到的散列地址,假设得到的另一个散列地址H1仍然发生冲突,只得继续求下一个地址H2,以些类推,直到Hk不发生冲突为止,则Hk为关键字在表中的地址。

1.开放定址法:指可存放新表项的空闲地址既向它的同义词表项开放,又向它的非同义词表项开放。其数学递推公式为:
Hi = (H(key) + di)%m
式中,H(key)为散列函数;i = 0,1,2,…,k(k<=m-1);m表示散列表表长;di为增量序列。
取定某一增量序列后,对应的处理方法就是确定的。通常有以下3种取法:
1.线性探测法。当di = 0,1,2,…,m-1时,称为线性探测法。这种方法的特点是:冲突发生时,顺序查看表中下一个单元(探测到表尾地址m-1时,下一个探测地址是表首地址0),直到找出一个空闲单元(当表未填满时一定能找到一个空闲单元)或查遍全表。
线性探测法可能使第i个散列地址的同义词存入第i+1个散列地址,这样本应存入第i+1个散列地址的元素就争夺第i+2个散列地址的元素的地址…从而造成大量元素在相邻的散列地址上”聚集“(或堆积)起来,大大降低了查找效率。

2.平方探测法:当di = 02,12,-12,22,-22,…,k2,-k2时,称为平方探测法,其中k<=m/2,散列表长度m必须是一个可以表示成4k+3的素数,又称二次探测法。平均探测法是一种处理冲突的较好方法,可以避免出现”堆积“问题,它的缺点是不能探测到散列表上的所有单元,但至少能探测到一半单元。

3.再散列法。当di=Hash2(key)时,称为再散列法,双称双散列法。需要使用两个散列函数。当通过第一个散列函数H(key)得到的地址发生冲突时,则利用第二个散列函数Hash2(key)计算该关键字的地址增量。它的具体散列函数形式如下:
Hi = (H(key) + i*Hash2(key))%m
初始探测位置H0 = H(key)%m,i是冲突的次数,初始为0。在再散列法中,最多经过m-1次探测就会遍历表中所有位置,回到H0位置。

注意:在开放定址的情形下,不能随便物理删除表中的已有元素,因为若删除元素,则会截断其他具有相同散列地址的元素的查找地址。因此,要删除一个元素时,可给它做一个删除标记,进行逻辑删除,但这样做的副作用是:执行多次删除后,表面上看起来散列表很满,实际上有许多位置未利用,因此需要定期维护散列表,要把删除标记的元素物理删除。

2.拉链法:对于不同的关键字可能会通过散列函数映射到同一地址,为了避免非同义词发生冲突,可以把所有的同义词存储在一个线性链表中,这个线性链表由其散列地址唯一标识。假设散列地址为i的同义词链表的头指针存放在散列表的第i个单元中,因而查找、插入和删除操作主要在同义词链中进行。拉链法适用于经常进行插入和删除的情况。例如,关键字序列为{19,14,23,01,68,20,84,27,55,11,10,79},散列函数H(key) = key%13,用拉链法处理冲突,建立的表如图所示:
在这里插入图片描述
散列查找及性能分析
对于一个给定的关键字key,根据散列函数可以计算出其散列地址,执行步骤如下:
初始化:addr = Hash(key);
1.检测查找表中地址为addr的位置上是否有记录,若无记录,返回查找失败;若有记录,比较它与key的值,若相等,则返回查找成功标志,否则执行步骤2。
2.用给定的处理冲突方法计算”下一个散列地址“,并把addr置为些地址,转入步骤1。

例如,关键字序列{19,14,23,01,68,20,84,27,55,11,10,79}按散列函数H(key) = key%13和线性探测处理冲突构造所得的散列表L如图所示:
在这里插入图片描述
查找各关键字的比较次数如图所示:
在这里插入图片描述

平均查找长度ASL为
ASL = (16+2+33+4+9)/12 = 2.5

从散列表的查找过程可见:
1.虽然散列表在关键字与记录的存储位置之间建立了直接映像,但由于”冲突“的产生,使得散列表的查找过程仍然是一个给定值和关键字进行比较的过程。因此,仍需要以平均查找长度作为衡量散列表的查找效率的度量。
2.散列表的查找效率取决于三个因素:散列函数,处理冲突的方法和装填因子。

排序
在这里插入图片描述
插入排序:其基本思想是每次将一个待排序的记录按其关键字大小插入前面已排好序的子序列,直到全部记录插入完成。由插入排序的思想可以引申出三个重要的排序算法:直接插入排序、折半插入排序和希尔排序。

1.直接插入排序
假设在排序过程中,待排序表L[1…n]在某次排序过程中的某一时刻状态如下:
在这里插入图片描述
要将元素L(i)插入已有序的子序列L[1…i-1],需要执行以下操作:
1.查找出L(i)在L[1…i-1]中的插入位置k。
2.将L[k…i-1]中的所有元素依次后移一个位置。
3.将L(i)复制到L(k)。
为了实现对L[1…n]的排序,可以将L(2)~L(n)依次插入前面已排好序的子序列,初始L[1]可以视为是一个已排好序的子序列。上述操作执行n-1次就能得到一个有序的表。插入排序在实现上通常采用就地排序(空间复杂度为O(1)),因而在从后向前的比较过程中,需要反复把已排序元素逐步向后挪位,为新元素提供插入空间。

void InsertSort(ElemType a[],int n){
	int i,j;
    for(i=1;i<n;i++){
        int temp = a[i];
        for(j = i-1;j>=0 && a[j]>temp;j--)  
            a[j+1] = a[j];
        a[j+1] = temp;
	}
}

直接插入排序算法性能分析如下:
空间效率:仅使用了常数个辅助单元,因而空间复杂度为O(1)。
时间效率:O(n2)。
稳定性:由于每次插入元素时总是从后向前比较再移动,所以不会出现相同元素相对位置发生变化的情况,即直接插入排序是一个稳定的排序方法。

折半插入算法
上个算法总是边比较边移动元素,于是可以这样操作,即先折半查找出元素的待插入位置,然后统一地移动待插入位置之后的元素。当排序表为顺序表时,可以对直接插入排序算法做如下改进:由于是顺序存储的线性表,所以查找有序子表时可以用折半查找来实现。确定待插入位置后,就可统一地向后移动元素。

void InsertSort(ElemType a[],int n){
	for(int i=1;i<n;i++){
        int temp = a[i];
        int l = 0;
        int r = i-1;
        while(l<=r){
            int mid = (l+r)/2;
            if(a[mid]>temp) r = mid-1;
            else l = mid+1}
        for(int j = i-1;j>=r+1;--j)
            a[j+1] = a[j];
        a[r+1] = temp;
   }
}

从上述算法中,不难看出折半插入排序仅减少了比较元素的次数,约为O(nlog2n),该比较次数与待排序表的初始状态无关,仅取决于表中的元素个数n;而元素的移动次数并未改变,它依赖于待排序表的初始状态。因此,折半插入排序的时间复杂度仍为O(n2)。

希尔排序
由前面可知,直接插入排序算法的时间复杂度为O(n),但若待排序为“正序”时,其时间复杂度可提高到O(n),由此可见它更适用于基本有序的排序表和数据量不大的排序表。希尔排序正是基于这两点分析对直接插入排序进行改进而得来的。又称缩小增量排序。
希尔排序的基本思想:先将待排序表分割成若干形如L[i,i+d,i+2d,…,i+kd]的“特殊”子表,即把相隔某个“增量”的记录组成一个子表,对各个子表分别进行直接插入排序,当整个表中的元素已呈“基本有序”时,再对全体记录进行一次直接插入排序。
希尔排序的过程如下:先取一个小于n的步长d1,把表中的全部记录分成d1组,所有距离为d1的倍数的记录放在同一组,在各组内进行直接插入排序;然后取第二个步长d2<d1,重复上述的过程,直到所取到的dt= 1,即所有记录已放在同一组中,再进行直接插入排序,由于此时已经具有较好的局部有序性,故可以很快得到最终结果。到目前为止,尚未求得一个最好的增量序列,希尔提出的方法是d1 = n/2,di+1 = (di/2上取整),并且最后一个增量等于1。
如图所示:第一趟取增量d1 = 5,将该序列分成5个子序列,分别对各子序列进行直接插入排序;第二趟取增量d2 = 3,分别对3个子序列进行直接插入排序。最后对整个序列进行一趟直接插入排序。
在这里插入图片描述
希尔排序算法的代码如下:

void shell_sort(int a[],int n){
	for(int gap = n/2; gap ; gap/=2){
		for(int i = gap;i<n;i++)
		{
			int x = a[i];
			int j;
			for(j = i; j>=gap && a[j-gap]>x; j-=gap)
				a[j] = a[j-gap];
			a[j] = x;
		}
	}
}

希尔排序算法的性能分析如下:
空间效率:仅使用了常数个辅助单元,因而空间复杂度为O(1)。
时间效率:当n在某个特定范围内时,希尔排序的时间复杂度约为O(n1.3)。在最坏情况下希尔排序的时间复杂度为O(n2)。
稳定性:当相同关键字的记录被划分到不同的子表时,可能会改变它们之间的相对次序。因此希尔排序是一种不稳定的排序方法。

交换排序:是指根据序列中的两个元素关键字的比较结果来对换这两个记录在序列中的位置。

冒泡排序
其基本思想是:从后往前(或从前往后)两两比较相邻元素的值,若为逆序(即A[i-1]>A[i]),则交换它们,直到序列比较完。我们称它为第一趟冒泡,结果是将最小的元素交换到待排序列的第一个位置(或将最大的元素交换到待排序列的最后一个位置)。下一趟冒泡时,前一趟确定的最小元素不再参与比较,每趟冒泡的结果是把序列中的最小元素(或最大元素)放到了序列的最终位置…这样最多做n-1趟冒泡就能把所有元素排好序。
如图是冒泡排序的示例
在这里插入图片描述
冒泡排序算法代码如下:

void BubbleSort(int a[],int n){
	for(int i=n-1;i>=1;i--){
		bool flag = true;
		for(int j = 1;j<=i;j++){
			if(a[j-1]>a[j])
			{
				swap(a[j-1],a[j]);
				flag = false;
			}
		}
		if(flag) return ;
	}
}

冒泡排序的性能分析如下:
空间效率:仅使用了常数个辅助单元,因而空间复杂度为O(1)。
时间效率:当初始序列有序时,显然第一趟冒泡后flag依然为true(本趟冒泡没有元素交换),从而直接跳出循环,比较次数为n-1,移动次数为0,从而最好情况下的时间复杂度为O(n);当初始序列为逆序时,需要进行n-1趟排序,第i趟排序要进行n-i将关键字比较,而且每次比较后都必须移动元素3次来交换元素位置。这种情况下
比较次数:(1~n-1)(n-i) = n(n-1)/2
移动次数:(1~n-1)3(n-i) = 3n(n-1)/2
从而,最坏情况下的时间复杂度为O(n2),其平均时间复杂也为O(n2)。
稳定性:由于i>j且a[i] == a[j] 时,不会发生交换,因此冒泡排序是一种稳定的排序方法。
注意:冒泡排序中所产生的有序子序列一定是全局有序的(不同于直接插入排序)。也就是説,有序子序列中的所有元素的关键字一定小于或大于无序子序列中所有元素的关键字,这样每趟排序都会将一个元素放置到其最终的位置上。

快速排序
快速排序的基本思想是基于分治的:在待排序表L[1…n]中仅取一个元素pivot作为枢轴(或基准,通常取首元素),通过一趟排序将待排序表划分为独立的两部分L[1…k-1]和L[k+1…n],使得L[1…k-1]中的所有元素小于pivot,L[k+1…n]中的所有元素大于等于pivot,则pivot放在了其最终位置L(k)上,这个过程称为一趟快速排序(或一次划分)。然后分别递归地对两个子表重复上述过程,直到每部分内只有一个元素或空为止,即所有元素放在其最终位置上。
在这里插入图片描述
快速排序算法代码如下:

void quick_sort(int a[],int l,int r){
	if(l>=r) return ;
	int i = l-1;
	int j = r+1;
	int x = a[l+r>>1];
	while(i<j){
		do i++; while(a[i]<x);
		do j--; while(a[j]>x);
		if(i<j) swap(a[i],a[j]);
	}
	quick_sort(a,l,j);
	quick_sort(a,j+1,r);
}

快速排序算法的性能分析如下:
空间效率:由于快速排序是递归的,需要借助一个递归工作栈来保存每层递归调用的必要信息,其容量应与递归调用的最大深度一致。最好情况下为O(log2n);最坏情况下,因为要进行n-1次递归调用,所以栈的深度为O(n);平均情况下,栈的深度为O(log2n)。
时间效率:最坏情况下时间复杂度O(n2)。最好情况下时间复杂度O(nlong2n)。
稳定性:在划分算法中,若右端区间有两个关键字相同,且均小于基准值的记录,则在交换到左端区间后,它的相对位置会发生变化,即快速排序是一种不稳定的排序方法。

选择排序
选择排序的基本思想是:每一趟(如第i趟)在后面n-i+1(i=1,2,3…,n-1)个待排序元素中选取关键字最小的元素,作为有序子序列的第i个元素,直到第n-1趟做完,待排序元素只剩下一个,就不用再选了。

简单选择排序
假设排序表为L[1…n],第i趟排序即从L[i…n]中选择关键字最小的元素与L(i)交换,每一趟排序可以确定一个元素的最终位置,这样经过n-1趟排序就可使得整个排序表有序。
简单选择排序算法的代码如下:

void SelectSort(int a[],int n){
	for(int i=0;i<n-1;i++){
		int min = i;
		for(int j = i+1;j<n;j++)
			if(a[j]<a[min]) min = j;
		if(min != i) swap(a[i],a[min]);
	}
}

简单选择排序算法的性能分析如下:
空间效率:仅使用常数个辅助单元,故空间效率为O(1)。
时间效率:在该算法中,元素移动的操作次数很少,不会超过3(n-1)次,最好的情况是移动0次,此时对应的表已经有序;但元素间比较次数与序列的初始状态无关,始终是n(n-1)/2次,因此时间复杂度始终是O(n2)。
稳定性:在第i趟找到最小元素后,和第i个元素交换,可能会导致第i个元素与其含有相同关键字元素的相对位置发生改变,因此简单选择排序是一种不稳定的排序方法。

堆排序
堆的定义如下:n个关键字序列L[1…n]称为堆,当且仅当该序列满足
1.L(i)>=L(2i)且L(i)>=L(2i+1)或
2.L(i)<=L(2i)且L(i)<=L(2i+1) (1<=i<=(n/2下取整))可以将一维数组视为一棵完全二叉树。
在这里插入图片描述

void down(int u)
{
    int t = u;
    if (u<<1 <= n && h[u<<1] < h[t]) t = u<<1;
    if ((u<<1|1) <= n && h[u<<1|1] < h[t]) t = u<<1|1;
    if (u != t)
    {
        swap(h[u], h[t]);
        down(t);
    }
}

int main()
{
    for (int i = 1; i <= n; i ++ ) cin >> h[i];
    for (int i = n/2; i; i -- ) down(i);
    while (true)
    {
        if (!n) break;
        cout << h[1] << ' ';
        h[1] = h[n];
        n -- ;
        down(1);
    }
    return 0;
}

在这里插入图片描述
在这里插入图片描述
注意:堆排序适合关键字较多的情况,例如,在1亿个数中选出前100个最大值?首先使用一个大小为100的数组,读入前100个数,建立小根堆,而后依次读入余下的数,若小于堆顶则舍弃,否则用该数取代堆顶并重新调整堆,待数据读取完毕,堆中100个数即为所示。
堆排序算法性能分析:
空间效率:仅使用了常数个辅助单元,所以空间复杂度为O(1)。
时间效率:建堆时间O(n),之后有n-1次向下调整操作,每次调整的时间复杂度为O(h),故在最好、最坏和平均情况下,堆排序的时间复杂度为O(nlog2n)。
稳定性:进行筛选时,有可能把后面相同关键字的元素调整到前面,所以堆排序算法是一种不稳定的排序算法。

归并排序
“归并”的含义是将两个或两个以上的有序表组合成一个新的有序表。假定待排序表含有n个记录,则可将其视为n个有序的子表,每个子表的长度为1,然后两两归并,得到(n/2上取整)个长度为2或1的有序表;继续两两归并…如此重复,直到合并成一个长度为n的有序表为止,这种排序方法称方2路归并排序。
在这里插入图片描述

void merge_sort(int l, int r)
{
    if (l >= r) return;
    int temp[N];
    int mid = l+r>>1;
    merge_sort(l, mid), merge_sort(mid+1, r);
    int k = 0, i = l, j = mid+1;
    while (i <= mid && j <= r)
    {
        if (a[i] < a[j]) temp[k ++ ] = a[i ++ ];
        else temp[k ++ ] = a[j ++ ];

    }
    while (i <= mid) temp[k ++ ] = a[i ++ ];
    while (j <= r) temp[k ++ ] = a[j ++ ];
    for (int i = l, j = 0; i <= r; i ++ , j ++ ) a[i] = temp[j];
}

2路归并排序算法的性能分析如下:
空间效率:辅助空间刚好为n个单元,所以算法的空间复杂度为O(n)。
时间效率:每趟归并的时间复杂度为O(n),其需进行(log2n上取整)趟归并,所以算法的时间复杂度为O(nlog2n)。
稳定性:由于合并操作不会改变相同关键字的相对次序,所以是稳定的排序方法

基数排序
在这里插入图片描述
算法代码

int maxbit()
{
    int maxv = a[0];
    for (int i = 1; i < n; i ++ )
        if (maxv < a[i])
            maxv = a[i];
    int cnt = 1;
    while (maxv >= 10) maxv /= 10, cnt ++ ;

    return cnt;
}
void radixsort()
{
    int t = maxbit();
    int radix = 1;

    for (int i = 1; i <= t; i ++ )
    {
        for (int j = 0; j < 10; j ++ ) count[j] = 0;
        for (int j = 0; j < n; j ++ )
        {
            int k = (a[j] / radix) % 10;
            count[k] ++ ;
        }
        for (int j = 1; j < 10; j ++ ) count[j] += count[j-1];
        for (int j = n-1; j >= 0; j -- )
        {
            int k = (a[j] / radix) % 10;
            temp[count[k]-1] = a[j];
            count[k] -- ;
        }
        for (int j = 0; j < n; j ++ ) a[j] = temp[j];
        radix *= 10;
    }

}

各种排序的性质
在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值