要点:
&
#include
#define MaxSize 10 //定义最大长度
typedef struct{
int data[MaxSize]; //用静态的“数组”存放数据元素 ElemType:int
int Length; //顺序表的当前长度
}SqList; //顺序表的类型定义
//基本操作——初始化一个顺序表
void InitList(SqList &L){
for(int i=0; i<MaxSize; i++){
L.data[i]=0; //将所有数据元素设置为默认初始值0,如果没有这一步,内存中会有遗留的“脏数据”
}
L.Length=0; //顺序表初始长度为0
}
int main(){
SqList L; //声明一个顺序表
//在内存里分配存储顺序表L的空间
//包括MaxSize*sizeof(ElemType)和存储length的空间
InitList(L); //初始化这个顺序表
//...
return 0;
}
malloc
函数:
L.data = (ElemType*)malloc(sizeof(ElemType)*InitSize)
其中(ElemType*)
可强制转换数据类型
#include //malloc,free函数的头文件
#define InitSize 10 //默认的最大长度
typedef struct{
int *data; //指示动态分配数组的指针
int MaxSize; //顺序表的最大容量
int length; //顺序表的当前长度
}SeqList;
void InitSize(SeqList &L){
L.data = (int*)malloc(sizeof(int)*InitSize); //用malloc函数申请一片连续的存储空间
L.length = 0;
L.MaxSize = InitSize;
}
int main(){
SeqList L;
InitSize(L);
//...其余操作
IncreaseSize(L,5);
return 0;
}
//增加动态数组的长度
void IncreaseSize(SeqList &L, int len){
int *p=L.data
L.data = (int*)malloc((L.MaxSize+len)*sizeof(int));
for(int i=0; i<L.length; i++){
L.data[i] = p[i] //将数据复制到新区域
}
L.MaxSize = L.MaxSize + len; //顺序表最大长度增加len
free(p); //释放原来的内存空间
}
ListInsert(&L,i,e)
基于静态分配的代码实现
#define MaxSize 10 //定义最大长度
typedef struct{
int data[MaxSize]; //用静态的“数组”存放数据元素 ElemType:int
int Length; //顺序表的当前长度
}SqList; //顺序表的类型定义
//基本操作——在L的位序i处插入元素e
bool ListInsert(SqList &L, int i, int e){
//判断i的范围是否有效
if(i<1||i>L.length+1)
return false;
if(L.length>MaxSize) //当前存储空间已满,不能插入
return false;
for(int j=L.length; j>i; j--){
//将第i个元素及其之后的元素后移
L.data[j]=L.data[j-1];
}
L.data[i-1]=e; //在位置i处放入e
L.length++; //长度加1
return true;
}
int main(){
SqList L; //声明一个顺序表
InitList(L); //初始化这个顺序表
//...插入几个元素
ListInsert(L,3,3);
return 0;
}
时间复杂度分析
L.data[j]=L.data[j-1]
的执行次数与问题规模n——L.length
的关系;插入到第i个位置 | 循环次数 |
---|---|
1 | n |
2 | n-1 |
3 | n-2 |
… | … |
n+1 | 0 |
平均循环次数 = np + (n-1)p + (n-2)p + … + 1×p = [ n(n+1)/2 ]×[ 1/(n+1) ] = n/2
平均时间复杂度 = O(n)
ListDelete(&L,i,e)
:删除表L中的第i个位置的元素,并用e返回删除元素的值
基于静态分配的代码实现
#define MaxSize 10 //定义最大长度
typedef struct{
int data[MaxSize]; //用静态的“数组”存放数据元素 ElemType:int
int Length; //顺序表的当前长度
}SqList; //顺序表的类型定义
bool LisDelete(SqList &L, int i, int &e){
// e用引用型参数
//判断i的范围是否有效
if(i<1||i>L.length)
return false;
e = L.data[i-1] //将被删除的元素赋值给e
for(int j=L.length; j>i; j--){
//将第i个后的元素前移
L.data[j-1]=L.data[j];
}
L.length--; //长度减1
return true;
}
int main(){
SqList L; //声明一个顺序表
InitList(L); //初始化这个顺序表
//...插入几个元素
int e = -1; //用变量e把删除的元素“带回来”
if(LisDelete(L,3,e))
printf("已删除第三个元素,删除元素值=%d\n",e);
else
printf("位序i不合法,删除失败\n");
return 0;
}
时间复杂度分析
L.data[j-1]=L.data[j]
的执行次数与问题规模n——L.length
的关系;删除第i个元素 | 循环次数 |
---|---|
1 | n-1 |
2 | n-2 |
3 | n-3 |
… | … |
n | 0 |
平均循环次数 = (n-1)p + (n-2)p + … + 1×p = [ n(n-1)/2 ]×[ 1/(n) ] = n-1/2
平均时间复杂度 = O(n)
GetElem(L,i)
: 按位查找操作——获取表L中第i个位置元素的值
基于静态分配的代码实现
#define MaxSize 10 //定义最大长度
typedef struct{
ElemType data[MaxSize]; //用静态的“数组”存放数据元素
int Length; //顺序表的当前长度
}SqList; //顺序表的类型定义
ElemType GetElem(SqList L, int i){
// ...判断i的值是否合法
return L.data[i-1]; //注意是i-1
}
基于动态分配的代码实现
#define InitSize 10 //顺序表的初始长度
typedef struct{
ElemType *data; //指示动态分配数组的指针
int MaxSize; //顺序表的最大容量
int length; //顺序表的当前长度
}SeqList;
ElemType GetElem(SqList L, int i){
// ...判断i的值是否合法
return L.data[i-1]; //就算是指针也能用数组下标哦!
}
时间复杂度分析
O(1)
由于顺序表的各个数据元素在内存中连续存放,因此可以根据起始地址和数据元素大小立即找到第i个元素———“随机存取”特性;
LocateElem(L, e)
: 按值查找操作,在表L中查找具有给定关键字值的元素;
基于动态分配的代码实现
#define InitSize 10 //定义最大长度
typedef struct{
ElemTyp *data; //用静态的“数组”存放数据元素
int Length; //顺序表的当前长度
}SqList;
//在顺序表L中查找第一个元素值等于e的元素,并返回其位序
int LocateElem(SqList L, ElemType e){
for(int i=0; i<L.lengthl i++)
if(L.data[i] == e)
return i+1; //数组下标为i的元素值等于e,返回其位序i+1
return 0; //推出循环,说明查找失败
}
Q: 如果顺序表里存放的是结构类型的数据元素,可不可以用 ==
进行比较?
A: 不能!结构类型的比较,需要依次对比各个分量来判断两个结构体是否相等;
例:
typedef struct{
int num;
int people;
}Customer;
void test(){
Customer a;
Customer b;
//...
if (a.num == b.num && a.people == b.people){
printf("相等");
}else{
printf("不相等");
}
}
时间复杂度分析
if(L.data[i] == e)
与问题规模n=L.length(表长)
的关系;目标元素所在位置i | 循环次数 |
---|---|
1 | 1 |
2 | 2 |
3 | 3 |
… | … |
n | n |
平均循环次数 = 1×1/n + 2×1/n +…+ n×1/n = [ n(n+1)/2 ] × 1/n = (n+1)/2
平均时间复杂度 = O(n)
struct LNode{
//定义单链表节点类型 LNode:结点
ElemType data; //每个结点存放一个数据元素 data:数据域
struct LNode *next; //指针指向下一个结点 next:指针域
};
增加一个新的结点:在内存中申请一个结点所需的空间,并用指针p指向这个结点
struct LNode* p = (struct LNode*) malloc(sizeof(struct LNode))
如果每次都要写struct很麻烦,所以可以利用typedef关键字——数据类型重命名:type<数据类型><别名>
Eg:
typedef int zhengshu;
typedef int *zhengshuzhizhen; //指向int型的指针
上面操作可以化简为:
typedef struct LNode LNode;
LNode* p = (LNode*) malloc(sizeof(LNode))
最简洁代码实现:
typedef struct LNode{
ElemType data;
struct LNode *next;
}LNode, *LinkList;
以上代码等同于:
struct LNode{
ElemType data;
struct LNode *next;
};
typedef struct LNode LNode; //重命名
typedef struct LNode *LinkList;
要表示一个单链表时,只需声明一个头指针L,指向单链表的第一个结点:
LNode *L; // 声明一个指向单链表第一个结点的指针,强调这是结点
//或者
LinkList L; // 声明一个指向单链表第一个结点的指针,强调这是链表
typedef struct LNode{
ElemType data;
struct LNode *next;
}LNode, *LinkList;
//初始化一个空的单链表
bool InitList(LinkList &L){
//注意用引用 &
L = NULL; //空表,暂时还没有任何结点;
return true;
}
void test(){
LinkList L; //声明一个指向单链表的指针: 头指针
//初始化一个空表
InitList(L);
//...
}
//判断单链表是否为空
bool Empty(LinkList L){
if (L == NULL)
return true;
else
return false;
}
typedef struct LNode{
ElemType data;
struct LNode *next;
}LNode, *LinkList;
//初始化一个单链表(带头结点)
bool InitList(LinkList &L){
L = (LNode*) malloc(sizeof(LNode)); //头指针指向的结点——分配一个头结点(不存储数据)
if (L == NULL) //内存不足,分配失败
return false;
L -> next = NULL; //头结点之后暂时还没有结点
return true;
}
void test(){
LinkList L; //声明一个指向单链表的指针: 头指针
//初始化一个空表
InitList(L);
//...
}
//判断单链表是否为空(带头结点)
bool Empty(LinkList L){
if (L->next == NULL)
return true;
else
return false;
}
不带头结点 V.S. 带头结点
1. 单链表的插入
ListInsert(&L, i, e)
: 在表L中的第i个位置上插入指定元素e = 找到第i-1个结点(前驱结点),将新结点插入其后;其中头结点可以看作第0个结点,故i=1时也适用。
代码实现
typedef struct LNode{
ElemType data;
struct LNode *next;
}LNode, *LinkList;
//在第i个位置插入元素e(带头结点)
bool ListInsert(LinkList &L, int i, ElemType e){
//判断i的合法性, i是位序号(从1开始)
if(i<1)
return False;
LNode *p; //指针p指向当前扫描到的结点
int j=0; //当前p指向的是第几个结点
p = L; //L指向头结点,头结点是第0个结点(不存数据)
//循环找到第i-1个结点
while(p!=NULL && j<i-1){
//如果i>lengh, p最后会等于NULL
p = p->next; //p指向下一个结点
j++;
}
if (p==NULL) //i值不合法
return false;
//在第i-1个结点后插入新结点
LNode *s = (LNode *)malloc(sizeof(LNode)); //申请一个结点
s->data = e;
s->next = p->next;
p->next = s; //将结点s连到p后,后两步千万不能颠倒qwq
return true;
}
时间复杂度分析
最好情况:插入第1个位置 O(1)
最坏情况:插入表尾 O(n)
平均时间复杂度 = O(n)
ListInsert(&L, i, e)
: 在表L中的第i个位置上插入指定元素e = 找到第i-1个结点(前驱结点),将新结点插入其后; 因为不带头结点,所以不存在“第0个”结点,因此!i=1 时,需要特殊处理——插入(删除)第1个元素时,需要更改头指针L;
代码实现
typedef struct LNode{
ElemType data;
struct LNode *next;
}LNode, *LinkList;
bool ListInsert(LinkList &L, int i, ElemType e){
if(i<1)
return false;
//插入到第1个位置时的操作有所不同!
if(i==1){
LNode *s = (LNode *)malloc(size of(LNode));
s->data =e;
s->next =L;
L=s; //头指针指向新结点
return true;
}
//i>1的情况与带头结点一样!唯一区别是j的初始值为1
LNode *p; //指针p指向当前扫描到的结点
int j=1; //当前p指向的是第几个结点
p = L; //L指向头结点,头结点是第0个结点(不存数据)
//循环找到第i-1个结点
while(p!=NULL && j<i-1){
//如果i>lengh, p最后会等于NULL
p = p->next; //p指向下一个结点
j++;
}
if (p==NULL) //i值不合法
return false;
//在第i-1个结点后插入新结点
LNode *s = (LNode *)malloc(sizeof(LNode)); //申请一个结点
s->data = e;
s->next = p->next;
p->next = s;
return true;
}
除非特别声明,否则之后的代码都默认为带头结点哦,做题注意审题
InsertNextNode(LNode *p, ElemType e)
: 给定一个结点p,在其之后插入元素e; 根据单链表的链接指针只能往后查找,故给定一个结点p,那么p之后的结点我们都可知,但是p结点之前的结点无法得知;
代码实现
typedef struct LNode{
ElemType data;
struct LNode *next;
}LNode, *LinkList;
bool InsertNextNode(LNode *p, ElemType e){
if(p==NULL){
return false;
}
LNode *s = (LNode *)malloc(sizeof(LNode));
//某些情况下分配失败,比如内存不足
if(s==NULL)
return false;
s->data = e; //用结点s保存数据元素e
s->next = p->next;
p->next = s; //将结点s连到p之后
return true;
} //平均时间复杂度 = O(1)
//有了后插操作,那么在第i个位置上插入指定元素e的代码可以改成:
bool ListInsert(LinkList &L, int i, ElemType e){
if(i<1)
return False;
LNode *p; //指针p指向当前扫描到的结点
int j=0; //当前p指向的是第几个结点
p = L; //L指向头结点,头结点是第0个结点(不存数据)
//循环找到第i-1个结点
while(p!=NULL && j<i-1){
//如果i>lengh, p最后会等于NULL
p = p->next; //p指向下一个结点
j++;
}
return InsertNextNode(p, e)
}
Q: 如何找到p结点的前驱节点?
A: 传入头指针L!就可以知道整个链表的信息了!
InsertPriorNode(LinkList L, LNode *p, ElemType e)
:循环查找p的前驱q,再对q进行后插操作,时间复杂度为O(n);
Q: 那如果不传入头指针L呢?
不传入头指针L的代码实现
//前插操作:在p结点之前插入元素e
bool InsertPriorNode(LNode *p, ElenType e){
if(p==NULL)
return false;
LNode *s = (LNode *)malloc(sizeof(LNode));
if(s==NULL) //内存分配失败
return false;
//重点来了!
s->next = p->next;
p->next = s; //新结点s连到p之后
s->data = p->data; //将p中元素复制到s
p->data = e; //p中元素覆盖为e
return true;
} //时间复杂度为O(1)
王道书版本代码
bool InsertPriorNode(LNode *p, LNode *s){
if(p==NULL || S==NULL)
return false;
s->next = p->next;
p->next = s; ///s连接到p
ELemType temp = p->data; //交换数据域部分
p->data = s->data;
s->data = temp;
return true;
}
2. 单链表的删除
ListDelete(&L, i, &e)
: 删除操作,删除表L中第i个位置的元素,并用e返回删除元素的值;头结点视为“第0个”结点;
思路:找到第i-1个结点,将其指针指向第i+1个结点,并释放第i个结点;
代码实现
typedef struct LNode{
ElemType data;
struct LNode *next;
}LNode, *LinkList;
bool ListDelete(LinkList &L, int i, ElenType &e){
if(i<1) return false;
LNode *p; //指针p指向当前扫描到的结点
int j=0; //当前p指向的是第几个结点
p = L; //L指向头结点,头结点是第0个结点(不存数据)
//循环找到第i-1个结点
while(p!=NULL && j<i-1){
//如果i>lengh, p最后会等于NULL
p = p->next; //p指向下一个结点
j++;
}
if(p==NULL)
return false;
if(p->next == NULL) //第i-1个结点之后已无其他结点
return false;
LNode *q = p->next; //令q指向被删除的结点
e = q->data; //用e返回被删除元素的值
p->next = q->next; //将*q结点从链中“断开”
free(q) //释放结点的存储空间
return true;
}
时间复杂度分析:
最坏,平均时间复杂度:O(n)
最好时间复杂度:删除第一个结点 O(1)
删除结点p,需要修改其前驱结点的next指针(两个方法);
"偷天换日"代码实现
bool DeleteNode(LNode *p){
if(p==NULL)
return false;
LNode *q = p->next; //令q指向*p的后继结点
p->data = p->next->data; //让p和后继结点交换数据域
p->next = q->next; //将*q结点从链中“断开”
free(q);
return true;
} //时间复杂度 = O(1)
但是 如果p是最后一个结点,那么p->next = q->next
and p->data = p->next->data
就会出错,只能从表头开始依次寻找o的前驱,时间复杂度为O(n); 这就是单链表的局限性——无法逆向检索。
3. 单链表的查找
探讨带头结点!
GetElem(L, i):
按位查找操作,获取表L中第i个位置的元素的值;
代码实现
LNode * GetElem(LinkList L, int i){
if(i<0) return NULL;
LNode *p; //指针p指向当前扫描到的结点
int j=0; //当前p指向的是第几个结点
p = L; //L指向头结点,头结点是第0个结点(不存数据)
while(p!=NULL && j<i){
//循环找到第i个结点
p = p->next;
j++;
}
return p; //返回p指针指向的值
}
平均时间复杂度 = O(n)
王道书版本代码
LNode * GetElem(LinkList L, int i){
int j=1; //从第一个结点开始
LNode *p = L->next //p先指向第一个结点
if(i==0) return L;
if(i<1) return NULL;
while(p!=NULL && j<i){
//循环找到第i个结点
p = p->next;
j++;
}
return p; //返回p指针指向的值
}
那么上一节的按位插入和按位删除就可以封装了!
LocateElem(L, e):
按值查找操作,在表L中查找具有给定关键字值的元素;
代码实现
LNode * LocateElem(LinkList L, ElemType e){
LNode *P = L->next; //p指向第一个结点
//从第一个结点开始查找数据域为e的结点
while(p!=NULL && p->data != e){
p = p->next;
}
return p; //找到后返回该结点指针,否则返回NULL
}
注意当ElemType是结构体时的操作
4. 求单链表的长度
Length(LinkList L)
代码实现 (带头结点)
int Length(LinkList L){
int len=0; //统计表长
LNode *p = L;
while(p->next != NULL){
p = p->next;
len++;
}
return len;
}
平均时间复杂度= O(n)
5. 单链表的建立
思路: 初始化一个单链表 -> 每取一个数据元素,插入到表尾/表头
核心: 初始化操作 and 指定结点的后插操作
探讨带头结点!
初始化单链表
设置变量length记录链表当前的长度
while循环{
每次取一个数据元素e;
ListInsert(L, length+1, e)插到尾部;
length++;
}
但是,因为每次都要执行↓,就是每次都要从头开始遍历,时间复杂度为O(n²)
while(p!=NULL && jnext;
j++;
}
解决方法:设置一个表尾指针r,对r这个结点进行后插操作InsertNextNode()
最终代码实现
LinkList List_TailInsert(LinkList &L){
//正向建立单链表
int x; //设ElemType为整型int
L = (LinkList)malloc(sizeof(LNode)); //建立头结点(初始化空表)
LNode *s, *r = L; //r为表尾指针
scanf("%d", &x); //输入要插入的结点的值
while(x!=9999){
//输入9999表结束
s = (LNode *)malloc(sizeof(LNode));
s->data = x;
r->next = s;
r = s //r指针指向新的表尾结点
scanf("%d", &x);
}
r->next = NULL; //尾结点指针置空
return L;
}
平均时间复杂度:O(n)
对头结点进行后插操作InsertNextNode()
初始化单链表
while循环{
每次取一个数据元素e;
InsertNextNode(L, e);
}
最终代码实现
LinkList List_HeadInsert(LinkList &L){
//逆向建立单链表
LNode *s;
int x;
L = (LinkList)malloc(sizeof(LNode)); //建立头结点
L->next = NULL; //初始为空链表,这步不能少!
scanf("%d", &x); //输入要插入的结点的值
while(x!=9999){
//输入9999表结束
s = (LNode *)malloc(sizeof(LNode)); //创建新结点
s->data = x;
r->next = L->next;
L->next = s; //将新结点插入表中,L为头指针
scanf("%d", &x);
}
return L;
}
PS: 只要是初始化单链表,都先将头指针指向NULL — L->next = NULL
;
详细见CSDN:单链表逆置—头插法图解
LNode *Inverse(LNode *L)
{
LNode *p, *q;
p = L->next; //p指针指向第一个结点
L->next = NULL; //头结点指向NULL
while (p != NULL){
q = p;
p = p->next;
q->next = L->next;
L->next = q;
}
return L;
双链表中节点类型的描述:
typedef struct DNode{
//定义双链表结点类型
ElemType data; //数据域
struct DNode *prior, *next; //前驱和后继指针
}DNode, *DLinklist;
typedef struct DNode{
//定义双链表结点类型
ElemType data; //数据域
struct DNode *prior, *next; //前驱和后继指针
}DNode, *DLinklist;
//初始化双链表
bool InitDLinkList(Dlinklist &L){
L = (DNode *)malloc(sizeof(DNode)); //分配一个头结点
if(L==NULL) //内存不足,分配失败
return false;
L->prior = NULL; //头结点的prior指针永远指向NULL
L->next = NULL; //头结点之后暂时还没有结点
return true;
}
void testDLinkList(){
//初始化双链表
DLinklist L; // 定义指向头结点的指针L
InitDLinkList(L); //申请一片空间用于存放头结点,指针L指向这个头结点
//...
}
//判断双链表是否为空
bool Empty(DLinklist L){
if(L->next == NULL) //判断头结点的next指针是否为空
return true;
else
return false;
}
与单链表中一样,DLinklist
强调链表, DNode *
强调结点,二者本质上等价;
InsertNextDNode(p, s)
: 在p结点后插入s结点
代码实现:
bool InsertNextDNode(DNode *p, DNode *s){
//将结点 *s 插入到结点 *p之后
if(p==NULL || s==NULL) //非法参数
return false;
s->next = p->next;
if (p->next != NULL) //p不是最后一个结点=p有后继结点
p->next->prior = s;
s->prior = p;
p->next = s;
return true;
}
思路:从头结点开始,找到某个位序的前驱结点,对该前驱结点执行后插操作;
思路:找到给定结点的前驱结点,再对该前驱结点执行后插操作;
删除p的后继结点q
p->next = q->next;
q->next->prior = p;
free(q);
如果要删除的结点q是最后一个结点,会出现错误,故增加条件判断以提高代码健壮性
代码实现
//删除p结点的后继结点
bool DeletNextDNode(DNode *p){
if(p==NULL) return false;
DNode *q =p->next; //找到p的后继结点q
if(q==NULL) return false; //p没有后继结点;
p->next = q->next;
if(q->next != NULL) //q结点不是最后一个结点
q->next->prior=p;
free(q);
return true;
}
//销毁一个双链表
bool DestoryList(DLinklist &L){
//循环释放各个数据结点
while(L->next != NULL){
DeletNextDNode(L); //删除头结点的后继结点
free(L); //释放头结点
L=NULL; //头指针指向NULL
}
}
while(p!=NULL){
//对结点p做相应处理,eg打印
p = p->next;
}
前向遍历
while(p!=NULL){
//对结点p做相应处理,eg打印
p = p->prior;
}
如果我们不想处理头结点,那就跳过头结点!
while(p->prior!=NULL){
//对结点p做相应处理
p = p->prior;
}
注意 双链表不可随机存取,按位查找和按值查找操作都只能用遍历的方式实现,时间复杂度为O(n)
1. 循环单链表
最后一个结点的指针不是NULL,而是指向头结点
typedef struct LNode{
ElemType data;
struct LNode *next;
}DNode, *Linklist;
/初始化一个循环单链表
bool InitList(LinkList &L){
L = (LNode *)malloc(sizeof(LNode)); //分配一个头结点
if(L==NULL) //内存不足,分配失败
return false;
L->next = L; //头结点next指针指向头结点
return true;
}
//判断循环单链表是否为空
bool Empty(LinkList L){
if(L->next == L)
return true; //为空
else
return false;
}
//判断结点p是否为循环单链表的表尾结点
bool isTail(LinkList L, LNode *p){
if(p->next == L)
return true;
else
return false;
}
单链表:从一个结点出发只能找到该结点后续的各个结点;对链表的操作大多都在头部或者尾部;设立头指针,从头结点找到尾部的时间复杂度=O(n),即对表尾进行操作需要O(n)的时间复杂度;
循环单链表:从一个结点出发,可以找到其他任何一个结点;设立尾指针,从尾部找到头部的时间复杂度为O(1),即对表头和表尾进行操作都只需要O(1)的时间复杂度;
可以让L指向表尾元素(插入,删除时可能需要修改L)
2. 循环双链表
表头结点的prior
指向表尾结点,表尾结点的next
指向头结点
typedef struct DNode{
ElemType data;
struct DNode *prior, *next;
}DNode, *DLinklist;
//初始化空的循环双链表
bool InitDLinkList(DLinklist &L){
L = (DNode *) malloc(sizeof(DNode)); //分配一个头结点
if(L==NULL) //内存不足,分配失败
return false;
L->prior = L; //头结点的prior指向头结点
L->next = L; //头结点的next指向头结点
}
void testDLinkList(){
//初始化循环单链表
DLinklist L;
InitDLinkList(L);
//...
}
//判断循环双链表是否为空
bool Empty(DLinklist L){
if(L->next == L)
return true;
else
return false;
}
//判断结点p是否为循环双链表的表尾结点
bool isTail(DLinklist L, DNode *p){
if(p->next == L)
return true;
else
return false;
}
对于循环双链表,操作 p->next->prior = s
不会出问题辣!因为就算p是最后一个结点,也不会出现空指针现象了(这个问题在双链表里会出现!)
bool InsertNextDNode(DNode *p, DNode *s){
s->next = p->next;
p->next->prior = s;
s->prior = p;
p->next = s;
和插入操作一样!q->next->prior
对于循环双链表不会出错了
//删除p的后继结点q
p->next = q->next;
q->next->prior = p;
free(q);
单链表: 各个结点散落在内存中的各个角落,每个结点有指向下一个节点的指针(下一个结点在内存中的地址);
静态链表——用数组的方式实现的链表: 分配一整片连续的内存空间,各个结点集中安置,包括了——数据元素and下一个结点的数组下标(游标)
addr
,则数据下标为2的存放地址为:addr
+8*2#define MaxSize 10 //静态链表的最大长度
struct Node{
//静态链表结构类型的定义
ElemType data; //存储数据元素
int next; //下一个元素的数组下标(游标)
};
//用数组定义多个连续存放的结点
void testSLinkList(){
struct Node a[MaxSize]; //数组a作为静态链表, 每一个数组元素的类型都是struct Node
//...
}
也可以这样:
#define MaxSize 10 //静态链表的最大长度
typedef struct{
//静态链表结构类型的定义
ELemType data; //存储数据元素
int next; //下一个元素的数组下标
}SLinkList[MaxSize];
void testSLinkList(){
SLinkList a;
}
上面这个代码等同于
#define MaxSize 10 //静态链表的最大长度
struct Node{
//静态链表结构类型的定义
ElemType data; //存储数据元素
int next; //下一个元素的数组下标(游标)
};
typedef struct Node SLinkList[MaxSize]; //重命名struct Node,用SLinkList定义“一个长度为MaxSize的Node型数组;
PS: SLinkList a
强调a是静态链表;struct Node a
强调a是一个Node型数组;
初始化静态链表:把a[0]
的next
设为-1
查找某个位序(不是数组下标,位序是各个结点在逻辑上的顺序)的结点:从头结点出发挨个往后遍历结点,时间复杂度O=(n)
在位序为i上插入结点:① 找到一个空的结点,存入数据元素;② 从头结点出发找到位序为i-1的结点;③修改新结点的next;④ 修改i-1号结点的next;
Q:如何判断结点为空?
A:在初始化时,将空闲结点的next设置为某个特殊值,eg:-2;
#define MaxSize 10 //静态链表的最大长度
typedef struct{
//静态链表结构类型的定义
ELemType data; //存储数据元素
int next; //下一个元素的数组下标
}SLinkList[MaxSize];
1. 逻辑结构
2. 存储结构
3. 基本操作 - 创
顺序表:需要预分配大片连续空间。若分配空间过小,则之后不方便拓展容量;若分配空间过大,则浪费内存资源;
malloc()
,free()
)链表:只需要分配一个头结点或者只声明一个头指针
4. 基本操作 - 销毁
顺序表:修改 Length
= 0
typedef struct{
ElemType *data;
int MaxSize;
int length;
}SeqList;
//创
L.data = (ELemType *)malloc(sizeof(ElemType) *InitSize)
//销
free(L.data);
//!malloc() 和 free() 必须成对出现
链表:依次删除各个结点 free()
5. 基本操作 - 增/删
顺序表:插入/删除元素要将后续元素后移/前移;时间复杂度=O(n),时间开销主要来自于移动元素;
链表:插入/删除元素只需要修改指针;时间复杂度=O(n),时间开销主要来自查找目标元素
6. 基本操作 - 查
顺序表
链表:
7. 开放式问题答题思路
Q: 请描述顺序表和链表的balabalabala…实现线性表时,用顺序表还是链表好?
A: 顺序表和链表的存储结构都是线性结构,都属于线性表;但是二者的存储结构不同,顺序表采用顺序存储…(特点,优缺点);链表采用链式存储…(特点,优缺点);由于采用不同的存储方式实现,因此基本操作的实现效率也不同;当初始化时…;当插入一个数据元素时…;当删除一个数据元素时…;当查找一个数据元素时…;
InitStack(&S)
初始化栈:构造一个空栈S,分配内存空间;DestroyStack(&S)
销毁栈:销毁并释放栈S所占用的内存空间;Push(&S, x)
进栈:若栈S未满,则将x加入使其成为新栈顶;Pop(&S, &x)
出栈:若栈S非空,则弹出(删除)栈顶元素,并用x返回;GetTop(S, &x)
读取栈顶元素:若栈S非空,则用x返回栈顶元素;(栈的使用场景大多只访问栈顶元素);StackEmpty(S)
判空: 断一个栈S是否为空,若S为空,则返回true
,否则返回false
;例:进栈顺序为:a -> b -> c -> d -> e
合法的出栈顺序:e d c b a / b e d c a (出栈和进栈交替进行) / ...
#define MaxSize 10 //定义栈中元素的最大个数
typedef struct{
ElemType data[MaxSize]; //静态数组存放栈中元素
int top; //栈顶元素
}SqStack;
void testStack(){
SqStack S; //声明一个顺序栈(分配空间)
//连续的存储空间大小为 MaxSize*sizeof(ElemType)
}
#define MaxSize 10 //定义栈中元素的最大个数
typedef struct{
ElemType data[MaxSize]; //静态数组存放栈中元素
int top; //栈顶元素
}SqStack;
//初始化栈
void InitStack(SqStack &S){
S.top = -1; //初始化栈顶指针
}
//判栈空
bool StackEmpty(SqStack S){
if(S.top == -1) //栈空
return true;
else //栈不空
return false;
}
//新元素进栈
bool Push(SqStack &S, ElemType x){
if(S.top == MaxSize - 1) //栈满
return false;
S.top = S.top + 1; //指针先加1
S.data[S.top] = x; //新元素入栈
/*
S.data[++S.top] = x;
*/
return true;
}
//出栈
bool Pop(SqStack &x, ElemType &x){
if(S.top == -1) //栈空
return false;
x = S.data[S.top]; //先出栈
S.top = S.top - 1; //栈顶指针减1
return true;
/*
x = S.data[S.top--];
*/
//只是逻辑上的删除,数据依然残留在内存里
}
//读栈顶元素
bool GetTop(SqStack S, ElemType &x){
if(S.top == -1)
return false;
x = S.data[S.top]; //x记录栈顶元素
return true;
}
void testStack(){
SqStack S; //声明一个顺序栈(分配空间)
InitStack(S);
//...
}
PS: 也可以初始化时定义 S.top = 0
:top指针指向下一个可以插入元素的位置(栈顶元素的后一个位置);
判空:if(S.top == 0)
进栈使用:S.data[S.top++] = x;
出栈使用:x = S.data[--S.top];
判断栈满:s.top == MaxSize
两个栈共享同一片空间
#define MaxSize 10 //定义栈中元素的最大个数
typedef struct{
ElemType data[MaxSize]; //静态数组存放栈中元素
int top0; //0号栈栈顶指针
int top1; //1号栈栈顶指针
}ShStack;
//初始化栈
void InitSqStack(ShStack &S){
S.top0 = -1; //初始化栈顶指针
S.top1 = MaxSize;
}
栈满条件:top0 + 1 == top1
因此,链栈实际上就是一个只能采用头插法插入或删除数据的链表;
typedef struct Linknode{
ElemType data; //数据域
struct Linknode *next; //指针域
}*LiStack; //栈类型的定义
参考:链栈基本操作(带头结点及不带头结点)
带有头结点的链栈基本操作
#include
struct Linknode{
int data; //数据域
Linknode *next; //指针域
}Linknode,*LiStack;
typedef Linknode *Node; //结点结构体指针变量
typedef Node List; //结点结构体头指针变量
//1. 初始化
void InitStack(LiStack &L){
//L为头指针
L = new Linknode;
L->next = NULL;
}
//2.判栈空
bool isEmpty(LiStack &L){
if(L->next == NULL){
return true;
}
else
return false;
}
//3. 进栈(:链栈基本上不会出现栈满的情况)
void pushStack(LiStack &L, int x){
Linknode s; //创建存储新元素的结点
s = new Linknode;
s->data = x;
//头插法
s->next = L->next;
L->next = s;
}
//4.出栈
bool popStack(LiStack &L, int &x){
Linknode s;
if(L->next == NULL) //栈空不能出栈
return false;
s = L->next;
x = s->data;
L->next = L->next->next;
delete(s);
return true;
}
不带头结点的链栈基本操作
#include
struct Linknode{
int data; //数据域
Linknode *next; //指针域
}Linknode,*LiStack;
typedef Linknode *Node; //结点结构体指针变量
typedef Node List; //结点结构体头指针变量
//1.初始化
void initStack(LiStack &L){
L=NULL;
}
//2.判栈空
bool isEmpty(LiStack &L){
if(L == NULL)
return true;
else
teturn false;
}
//3.进栈
void pushStack(LiStack &L, int x){
Linknode s; //创建存储新元素的结点
s = new Linknode;
s->next = L;
L = s;
}
//4.出栈
bool popStack(LiStack &L, int &x){
Linknode s;
if(L = NULL) //栈空不出栈
return false;
s = L;
x = s->data;
L = L->next;
delete(s);
return true;
}
InitQueue(&Q)
: 初始化队列,构造一个空列表QDestroyQueue(&Q)
: 销毁队列,并释放队列Q所占用的内存空间EnQueue(&Q, x)
: 入队,若队列Q未满,将x加入,使之成为新的队尾DeQueue(&Q, &x)
: 出队,若队列Q非空,删除队头元素,并用x返回GetHead(Q,&x)
: 读队头元素,若队列Q非空,则将队头元素赋值给xQueueEmpty(Q)
: 判队列空,若队列Q为空,则返回true队头指针:指向队头元素;
队尾指针:指向队尾元素的后一个位置(下一个应该插入的位置)
//队列的顺序存储类型
# define MaxSize 10; //定义队列中元素的最大个数
typedef struct{
ElemType data[MaxSize]; //用静态数组存放队列元素
//连续的存储空间,大小为——MaxSize*sizeof(ElemType)
int front, rear; //队头指针和队尾指针
}SqQueue;
//初始化队列
void InitQueue(SqQueue &Q){
//初始化时,队头、队尾指针指向0
Q.rear = Q.front = 0;
}
void test{
SqQueue Q; //声明一个队列
InitQueue(Q);
//...
}
// 判空
bool QueueEmpty(SqQueue 0){
if(Q.rear == Q.front) //判空条件后
return true;
else
return false;
}
Q: 能否用 Q.rear == MaxSize
作为队列满的条件?
A: 不能!会有假溢出, 所以需要用 模运算 将存储空间 {0,1,2,…,MaxSize} 在逻辑上变成“环状”——循环队列!
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7CPbzxxS-1620744230788)(循环队列.PNG)]
a%b == a除以b的余数
初始:Q.front = Q.rear = 0;
队首指针进1:Q.front = (Q.front + 1) % MaxSize
队尾指针进1:Q.rear = (Q.rear + 1) % MaxSize —— 队尾指针后移,当移到最后一个后,下次移动会到第一个位置
队列长度:(Q.rear + MaxSize - Q.front) % MaxSize
Q: 能否用Q.rear == Q.front
作为队列满的条件?
A: 不能!这已经作为队列空的判断条件了;
队尾指针的再下一个位置就是队头,即 (Q.rear+1)%MaxSize == Q.front
bool EnQueue(SqQueue &Q, ElemType x){
if((Q.rear+1)%MaxSize == Q.front) //队满
return false;
Q.data[Q.rear] = x; //将x插入队尾
Q.rear = (Q.rear + 1) % MaxSize; //队尾指针加1取模
return true;
}
//出队,删除一个队头元素,用x返回
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;
}
bool GetHead(SqQueue &Q, ElemType &x){
if(Q.rear == Q.front) //队空报错
return false;
x = Q.data[Q.front];
return true;
}
定义一个变量 size
用于记录队列此时记录了几个数据元素,初始化 size = 0
,进队成功 size++
,出队成功size--
,根据size的值判断队满与队空
队满条件:size == MaxSize
队空条件:size == 0
# define MaxSize 10;
typedef struct{
ElemType data[MaxSize];
int front, rear;
int size; //队列当前长度
}SqQueue;
//初始化队列
void InitQueue(SqQueue &Q){
Q.rear = Q.front = 0;
size = 0;
}
定义一个变量 tag
,tag = 0
--最近进行的是删除操作;tag = 1
--最近进行的是插入操作;
tag = 0
;只有删除操作,才可能导致队空;tag = 1
;只有插入操作,才可能导致队满;队满条件:Q.front == Q.rear && tag == 1
队空条件:Q.front == Q.rear && tag == 0
# define MaxSize 10;
typedef struct{
ElemType data[MaxSize];
int front, rear;
int tag; //最近进行的是删除or插入
}SqQueue;
(Q.rear + 1) % MaxSize == Q.front
判满
入队操作
Q.rear = (Q.rear + 1) % MaxSize; //后移一位
Q.data[Q.rear] = x;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bJHGyMSI-1620744230792)(队列顺序实现.PNG)]
typedef struct LinkNode{
//链式队列结点
ElemType data;
struct LinkNode *next;
}
typedef struct{
//链式队列
LinkNode *front, *rear; //队列的队头和队尾指针
}LinkQueue;
void InitQueue(LinkQueue &Q){
//初始化时,front、rear都指向头结点
Q.front = Q.rear = (LinkNode*)malloc(sizeof(LinkNode));
Q.front -> next = NULL;
}
//判断队列是否为空
bool IsEmpty(LinkQueue Q){
if(Q.front == Q.rear) //也可用 Q.front -> next == NULL
return true;
else
return false;
}
//新元素入队 (表尾进行)
void EnQueue(LinkQueue &Q, ElemType x){
LinkNode *s = (LinkNode *)malloc(sizeof(LinkNode)); //申请一个新结点
s->data = x;
s->next = NULL; //s作为最后一个结点,指针域指向NULL
Q.rear->next = s; //新结点插入到当前的rear之后
Q.rear = s; //表尾指针指向新的表尾
}
//队头元素出队
bool DeQueue(LinkQueue &Q, ElemType &x){
if(Q.front == Q.rear)
return false; //空队
LinkNode *p = Q.front->next; //p指针指向即将删除的结点 (头结点所指向的结点)
x = p->data;
Q.front->next = p->next; //修改头结点的next指针
if(Q.rear == p) //此次是最后一个结点出队
Q.rear = Q.front; //修改rear指针
free(p); //释放结点空间
return true;
}
顺序存储:预分配存储空间
链式存储:一般不会队满,除非内存不足
设置一个int length
记录链式队列长度
void InitQueue(LinkQueue &Q){
//初始化时,front、rear都指向NULL
Q.front = NULL;
Q.rear = NULL;
}
//判断队列是否为空
bool IsEmpty(LinkQueue Q){
if(Q.front == NULL) //也可以用 Q.rear == NULL
return true;
else
return false;
}
//新元素入队 (表尾进行)
void EnQueue(LinkQueue &Q, ElemType x){
LinkNode *s = (LinkNode *)malloc(sizeof(LinkNode)); //申请一个新结点
s->data = x;
s->next = NULL;
//第一个元素入队时需要特别处理
if(Q.front = NULL){
//在空队列中插入第一个元素
Q.front = s; //修改队头队尾指针
Q.rear = s;
}else{
Q.rear->next = s; //新结点插入到rear结点之后
Q.rear = s; //修改rear指针指向新的表尾结点
}
}
例: 数据元素输入序列为 1,2,3,4
,判断 4!=24 个输出序列的合法性
PS: 栈中合法的序列,双端队列中一定也合法
栈 | 输入受限的双端队列 | 输出受限的双端队列 |
---|---|---|
14个合法(卡特兰数) | 验证在栈中不合法的序列 | 验证在栈中不合法的序列 |
只有 4213 和 4231 不合法 | 只有 4132 和 4231 不合法 |
用栈实现括号匹配
((()))
最后出现的左括号最先被匹配 (栈的特性—LIFO);匹配失败情况:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UBr5m1ue-1622726764306)(括号匹配.png)]
#define MaxSize 10
typedef struct{
char data[MaxSize];
int top;
} SqStack;
//初始化栈
InitStack(SqStack &S)
//判断栈是否为空
bool StackEmpty(SqStack &S)
//新元素入栈
bool Push(SqStack &S, char x)
//栈顶元素出栈,用x返回
bool Pop(SqStack &S, char &x)
bool bracketCheck(char str[], int length){
SqStack S; //声明
InitStack(S); //初始化栈
for(int i=0; i<length; i++){
if(str[i] == '(' || str[i] == '[' || str[i] == '{'){
Push(S, str[i]); //扫描到左括号,入栈
}else{
if(StackEmpty(S)) //扫描到右括号,且当前栈空
return false; //匹配失败
char topElem; //存储栈顶元素
Pop(S, topElem); //栈顶元素出栈
if(str[i] == ')' && topElem != '(' )
return false;
if(str[i] == ']' && topElem != '[' )
return false;
if(str[i] == '}' && topElem != '{' )
return false;
}
}
StackEmpty(S); //栈空说明匹配成功
}
运算符在两个操作数中间:
① a + b
② a + b - c
③ a + b - c*d
④ ((15 ÷ (7-(1+1)))×3)-(2+(1+1))
⑤ A + B × (C - D) - E ÷ F
运算符在两个操作数后面:
① a b +
② ab+ c - / a bc- +
③ ab+ cd* -
④ 15 7 1 1 + - ÷ 3 × 2 1 1 + + -
⑤ A B C D - × + E F ÷ - (机算结果)
A B C D - × E F ÷ - + (不选择)
步骤1: 确定中缀表达式中各个运算符的运算顺序
步骤2: 选择下一个运算符,按照[左操作数 右操作数 运算符]
的方式组合成一个新的操作数
步骤3: 如果还有运算符没被处理,继续步骤2
“左优先”原则: 只要左边的运算符能先计算,就优先算左边的 (保证运算顺序唯一);
中缀:A + B - C * D / E + F
① ④ ② ③ ⑤
后缀:A B + C D * E / - F +
初始化一个栈,用于保存暂时还不能确定运算顺序的运算符。从左到右处理各个元素,直到末尾。可能遇到三种情况:
'('
直接入栈; 遇到 ')'
则依次弹出栈内运算符并加入后缀表达式,直到弹出 '('
为止。注意: '('
不加入后缀表达式。'('
或栈空则停止。之后再把当前运算符入栈。按上述方法处理完所有字符后,将栈中剩余运算符依次弹出,并加入后缀表达式。
从左往右扫描,每遇到一个运算符,就让运算符前面最近的两个操作数执行对应的运算,合体为一个操作数;
注意: 两个操作数的左右顺序
用栈实现后缀表达式的计算(栈用来存放当前暂时不能确定运算次序的操作数)
步骤1: 从左往后扫描下一个元素,直到处理完所有元素;
步骤2: 若扫描到操作数,则压入栈,并回到步骤1
;否则执行步骤3
;
步骤3: 若扫描到运算符,则弹出两个栈顶元素,执行相应的运算,运算结果压回栈顶,回到步骤1
;
注意: 先出栈的是“右操作数”
运算符在两个操作数前面:
① + a b
② - +ab c
③ - +ab *cd
步骤1: 确定中缀表达式中各个运算符的运算顺序
步骤2: 选择下一个运算符,按照[运算符 左操作数 右操作数]
的方式组合成一个新的操作数
步骤3: 如果还有运算符没被处理,就继续执行步骤2
“右优先”原则: 只要右边的运算符能先计算,就优先算右边的;
中缀:A + B * (C - D) - E / F
⑤ ③ ② ④ ①
前缀:+ A - * B - C D / E F
用栈实现前缀表达式的计算
步骤1: 从右往左扫描下一个元素,直到处理完所有元素;
步骤2: 若扫描到操作数则压入栈,并回到步骤1
,否则执行步骤3
步骤3: 若扫描到运算符,则弹出两个栈顶元素,执行相应运算,运算结果压回栈顶,回到步骤1
;
注意: 先出栈的是“左操作数”
两个算法的结合: 中缀转后缀 + 后缀表达式的求值
初始化两个栈,操作数栈 和运算符栈
若扫描到操作数,压人操作数栈
若扫描到运算符或界限符,则按照“中缀转后缀”相同的逻辑压入运算符栈 (期间也会弹出运算符,每当弹出一个运算符时,就需要再弹出两个操作数栈的栈项元素并执行相应运算,运算结果再压回操作数栈)
函数调用的特点:最后被调用的函数最先执行结束(LIFO)
函数调用时,需要用一个栈存储:
递归调用时,函数调用栈称为 “递归工作栈”:
缺点:太多层递归可能回导致栈溢出;
适合用“递归”算法解决:可以把原始问题转换为属性相同,但规模较小的问题;
(详见“树”章节)
(详见“图”章节)
多个进程争抢着使用优先的系统资源时,FCFS(先来先服务)是一种常用策略
Eg: CPU资源分配、打印数据缓冲区
Elemtype a[10];
各数组元素大小相同,物理上连续存放;
起始地址:LOC
数组下标:默认从0开始!
数组元素 a[i]
的存放地址 = LOC + i × sizeof(ElemType)
Elemtype b[2][4]; //2行4列的二维数组
行优先/列优先存储优点:实现随机存储
起始地址:LOC
M行N列的二维数组 b[M][N]
中,b[i][j]
的存储地址:
LOC + (i×N + j) × sizeof(ElemType)
LOC + (j×M + i) × sizeof(ElemType)
二维数组存储
1
开始;0
开始;特殊矩阵——压缩存储空间
对称矩阵(方阵)
三角矩阵(方阵)
三对角矩阵(方阵)
稀疏矩阵
顺序存储——三元组
链式存储——十字链表法
S = 'iPhone 11 Pro Max?'
;M = ''
是空串;N = ' '
是空格串;假设有串 T = ''
, S = 'iPhone 11 Pro Max?'
, W = 'Pro'
StrAssign(&T, chars)
: 赋值操作,把串T赋值为chars;StrCopy(&T, S)
: 复制操作,把串S复制得到串T;StrEmpty(S)
: 判空操作,若S为空串,则返回TRUE,否则返回False;StrLength(S)
: 求串长,返回串S的元素个数;
返回
length
值
ClearString(&S)
: 清空操作,将S清为空串;
将
length = 0
,逻辑上清空,但是内存中还有
DestroyString(&S)
: 销毁串,将串S销毁——回收存储空间;Concat(&T, S1, S2)
: 串联联接,用T返回由S1和S2联接而成的新串———可能会导致存储空间的扩展;
Concat(&T, S, W)
T = ‘iPhone 11 Pro Max?Pro’
SubString(&Sub, S, pos, len)
: 求子串,用Sub返回串S的第pos个字符起长度为len的子串;
SubString(&T, S, 4, 6)
T = ‘one 11’
Index(S, T)
: 定位操作,若主串S中存在与串T值相同的子串,则返回它再主串S中第一次出现的位置,否则函数值为0;
Index(S, T)
11
StrCompare(S, T)
: 串的比较操作,参照英文词典排序方式;若S > T,返回值>0; S = T,返回值=0 (需要两个串完全相同) ; S < T,返回值<0;#define MAXLEN 255 //预定义最大串长为255
typedef struct{
char ch[MAXLEN]; //静态数组实现(定长顺序存储)
//每个分量存储一个字符
//每个char字符占1B
int length; //串的实际长度
}SString;
串长的两种表示法:
length
来存放串的长度(保留ch[0]
);ch[0]
充当length
;
length
变量,以字符'\0'
表示结尾(对应ASCII码的0);
ch[0]
废弃不用,声明int型变量length
来存放串的长度(方案一与方案二的结合)基本操作实现(基于方案四)
#define MAXLEN 255
typedef struct{
char ch[MAXLEN];
int length;
}SString;
// 1. 求子串
bool SubString(SString &Sub, SString S, int pos, int len){
//子串范围越界
if (pos+len-1 > S.length)
return false;
for (int i=pos; i<pos+len; i++)
Sub.cn[i-pos+1] = S.ch[i];
Sub.length = len;
return true;
}
// 2. 比较两个串的大小
int StrCompare(SString S, SString T){
for (int i; i<S.length && i<T.length; i++){
if(S.ch[i] != T.ch[i])
return S.ch[i] - T.ch[i];
}
//扫描过的所有字符都相同,则长度长的串更大
return S.length - T.length;
}
// 3. 定位操作
int Index(SString S, SString T){
int i=1;
n = StrLength(S);
m = StrLength(T);
SString sub; //用于暂存子串
while(i<=n-m+1){
SubString(Sub,S,i,m);
if(StrCompare(Sub,T)!=0)
++i;
else
return i; // 返回子串在主串中的位置
}
return 0; //S中不存在与T相等的子串
}
ps:结合顺序表思考优缺点
//动态数组实现
typedef struct{
char *ch; //按串长分配存储区,ch指向串的基地址
int length; //串的长度
}HString;
HString S;
S.ch = (char *) malloc(MAXLINE * sizeof(char)); //基地址指针指向连续空间的起始位置
//malloc()需要手动free()
S.length;
typedef struct StringNode{
char ch; //每个结点存1个字符
struct StringNode *next;
}StringNode, * String;
问题:存储密度低,每个字符1B,每个指针4B;
解决方案:每一个链表的结点存储多个字符——每个结点称为块——块链结构
typedef struct StringNode{
char ch[4]; //每个结点存多个个字符
struct StringNode *next;
}StringNode, * String;
ps:结合链表思考优缺点
int Index(SString S, SString T){
int i=1; //扫描主串S
int j=1; //扫描模式串T
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+1
个子串O(nm)
O((n-m+1)m) = O(nm - m^2 + m) = O(nm)
O(n)
O(n-m+1) = O(n)
next数组
(只与模式串有关,与主串无关),利用next数组
进行匹配,当匹配失败时,主串的指针 i
不再回溯!
当第一个元素匹配失败时,匹配下一个相邻子串,令
j=0,i++,j++
next数组
(会手算即可)j
个字符失配时,从模式串的第next[j]
继续往后匹配;next[1] = 0
——表示模式串应右移一位,主串当前指针后移一位,再和模式串的第一字符进行比较;next[2] = 0
;例:对于串 T = 'abaabc'
next[0] | next[1] | next[2] | next[3] | next[4] | next[5] | next[6] |
---|---|---|---|---|---|---|
0 | 1 | 1 | 2 | 2 | 3 |
next数组
进行模式匹配int Index_KMP(SString S, SString T, int next[]){
int i=1; //主串
int j=1; //模式串
while(i<S.length && j<=T.length){
if(j==0 || S.ch[i]==T.ch[j]){
//第一个元素匹配失败时
++j;
++i; //继续比较后继字符
}
else
j=next[j] //模式串向右移动
}
if(j>T.length)
return i-T.length; //匹配成功
}
3. 时间复杂度分析:
O(m)
O(n)
O(m+n)
m
的数、m
叉数的区别度为 m 的树 |
m 叉树 |
---|---|
树的度:m 为各结点的度的最大值 |
m 叉树:每个结点最多只能有 m 个孩子的树 |
任意结点的度 ≦ m | 任意结点的度 ≦ m |
至少有一个结点度 = m | 允许所有结点的度 < m |
一定是非空树,至少有m+1个结点 | 可以是空树 |
第i层至多有 m^(i-1) 个结点 |
第i层至多有 m^(i-1) 个结点 |
高度为h 、度为 m 的树至少有h+m-1 个结点 |
高度为h 的m 叉树至多有(m^h-1)/(m-1) 个结点;至少有h 个结点 |
具有n 个结点的m 叉树,最小高度为⌈ logm(n(m-2)+1)⌉ |
二叉树的顺序存储中,一定要把二叉树的结点编号与完全二叉树对应起来;
#define MaxSize 100
struct TreeNode{
ElemType value; //结点中的数据元素
bool isEmpty; //结点是否为空
}
main(){
TreeNode t[MaxSize];
for (int i=0; i<MaxSize; i++){
t[i].isEmpty = true;
}
}
考点:
i
的左孩子:2i
i
的右孩子:2i + 1
i
的父节点:⌊i/2⌋
i
所在的层次:⌊log2n + 1⌋
or ⌈ log2n+1)⌉
若完全二叉树中共有n个结点(非完全二叉树不能用)
i
是否有左孩子:2i ≦ n
i
是否有右孩子:2i+1 ≦ n
i
是否时叶子/分支结点:i > ⌊n/2⌋
最坏情况: 高度为h且只有h个结点的单支树(所有结点只有右孩子),也至少需要2^h-1个存储单元;
结论: 二叉树的顺序存储结构,只适合存储完全二叉树和满二叉树
//二叉树的结点
struct ElemType{
int value;
};
typedef struct BiTnode{
ElemType data; //数据域
struct BiTNode *lchild, *rchild; //左、右孩子指针
}BiTNode, *BiTree;
//定义一棵空树
BiTree root = NULL;
//插入根节点
root = (BiTree) malloc (sizeof(BiTNode));
root -> data = {
1};
root -> lchild = NULL;
root -> rchild = NULL;
//插入新结点
BiTNode *p = (BiTree) malloc (sizeof(BiTNode));
p -> data = {
2};
p -> lchild = NULL;
p -> rchild = NULL;
root -> lchild = p; //作为根节点的左孩子
typedef struct BiTnode{
ElemType data; //数据域
struct BiTNode *lchild, *rchild; //左、右孩子指针
struct BiTNode *parent; //父节点指针
}BiTNode, *BiTree;
typedef struct BiTnode{
ElemType data;
struct BiTNode *lchild, *rchild;
}BiTNode, *BiTree;
void PreOrder(BiTree T){
if(T!=NULL){
visit(T); //访问根结点
PreOrder(T->lchild); //递归遍历左子树
PreOrder(T->rchild); //递归遍历右子树
}
}
空间复杂度: O(h)
typedef struct BiTnode{
ElemType data;
struct BiTNode *lchild, *rchild;
}BiTNode, *BiTree;
void InOrder(BiTree T){
if(T!=NULL){
InOrder(T->lchild); //递归遍历左子树
visit(T); //访问根结点
InOrder(T->rchild); //递归遍历右子树
}
}
typedef struct BiTnode{
ElemType data;
struct BiTNode *lchild, *rchild;
}BiTNode, *BiTree;
void PostOrder(BiTree T){
if(T!=NULL){
PostOrder(T->lchild); //递归遍历左子树
PostOrder(T->rchild); //递归遍历右子树
visit(T); //访问根结点
}
}
算法思想:
//二叉树的结点(链式存储)
typedef struct BiTnode{
ElemType data;
struct BiTNode *lchild, *rchild;
}BiTNode, *BiTree;
//链式队列结点
typedef struct LinkNode{
BiTNode * data;
typedef LinkNode *next;
}LinkNode;
typedef struct{
LinkNode *front, *rear;
}LinkQueue;
//层序遍历
void LevelOrder(BiTree T){
LinkQueue Q;
InitQueue (Q); //初始化辅助队列
BiTree p;
EnQueue(Q,T); //将根节点入队
while(!isEmpty(Q)){
//队列不空则循环
DeQueue(Q,p); //队头结点出队
visit(p); //访问出队结点
if(p->lchild != NULL)
EnQueue(Q,p->lchild); //左孩子入队
if(p->rchild != NULL)
EnQueue(Q,p->rchild); //右孩子入队
}
}
key: 找到树的根节点,并根据中序序列划分左右子树,再找到左右子树根节点、
线索二叉树的概念与作用
线索二叉树的存储结构
//线索二叉树结点
typedef struct ThreadNode{
ElemType data;
struct ThreadNode *lchild, *rchild;
int ltag, rtag; // 左、右线索标志
}ThreadNode, *ThreadTree;
tag == 0: 指针指向孩子
tag == 1: 指针是“线索”
先序线索二叉树——线索指向先序前驱、先序后继
后序线索二叉树——线索指向后序前驱、后序后继
typedef struct ThreadNode{
int data;
struct ThreadNode *lchild, *rchild;
int ltag, rtag; // 左、右线索标志
}ThreadNode, *ThreadTree;
//全局变量pre, 指向当前访问的结点的前驱
TreadNode *pre=NULL;
void InThread(ThreadTree T){
if(T!=NULL){
InThread(T->lchild); //中序遍历左子树
visit(T); //访问根节点
InThread(T->rchild); //中序遍历右子树
}
}
void visit(ThreadNode *q){
if(q->lchid = NULL){
//左子树为空,建立前驱线索
q->lchild = pre;
q->ltag = 1;
}
if(pre!=NULL && pre->rchild = NULL){
pre->rchild = q; //建立前驱结点的后继线索
pre->rtag = 1;
}
pre = q;
}
//中序线索化二叉树T
void CreateInThread(ThreadTree T){
pre = NULL; //pre初始为NULL
if(T!=NULL);{
//非空二叉树才能进行线索化
InThread(T); //中序线索化二叉树
if(pre->rchild == NULL)
pre->rtag=1; //处理遍历的最后一个结点
}
}
注意【转圈】问题,当ltag==0
时,才能对左子树先序线索化
typedef struct ThreadNode{
int data;
struct ThreadNode *lchild, *rchild;
int ltag, rtag; // 左、右线索标志
}ThreadNode, *ThreadTree;
//全局变量pre, 指向当前访问的结点的前驱
TreadNode *pre=NULL;
//先序遍历二叉树,一边遍历一边线索化
void PreThread(ThreadTree T){
if(T!=NULL){
visit(T);
if(T->ltag == 0) //lchild不是前驱线索
PreThread(T->lchild);
PreThread(T->rchild);
}
}
void visit(ThreadNode *q){
if(q->lchid = NULL){
//左子树为空,建立前驱线索
q->lchild = pre;
q->ltag = 1;
}
if(pre!=NULL && pre->rchild = NULL){
pre->rchild = q; //建立前驱结点的后继线索
pre->rtag = 1;
}
pre = q;
}
//先序线索化二叉树T
void CreateInThread(ThreadTree T){
pre = NULL; //pre初始为NULL
if(T!=NULL);{
//非空二叉树才能进行线索化
PreThread(T); //先序线索化二叉树
if(pre->rchild == NULL)
pre->rtag=1; //处理遍历的最后一个结点
}
}
typedef struct ThreadNode{
int data;
struct ThreadNode *lchild, *rchild;
int ltag, rtag; // 左、右线索标志
}ThreadNode, *ThreadTree;
//全局变量pre, 指向当前访问的结点的前驱
TreadNode *pre=NULL;
//先序遍历二叉树,一边遍历一边线索化
void PostThread(ThreadTree T){
if(T!=NULL){
PostThread(T->lchild);
PostThread(T->rchild);
visit(T); //访问根节点
}
}
void visit(ThreadNode *q){
if(q->lchid = NULL){
//左子树为空,建立前驱线索
q->lchild = pre;
q->ltag = 1;
}
if(pre!=NULL && pre->rchild = NULL){
pre->rchild = q; //建立前驱结点的后继线索
pre->rtag = 1;
}
pre = q;
}
//先序线索化二叉树T
void CreateInThread(ThreadTree T){
pre = NULL; //pre初始为NULL
if(T!=NULL);{
//非空二叉树才能进行线索化
PostThread(T); //后序线索化二叉树
if(pre->rchild == NULL)
pre->rtag=1; //处理遍历的最后一个结点
}
}
中序线索二叉树找中序后继:在中序线索二叉树中找到指定节点 *p
的中序后继 next
若
p->rtag == 1
, 则next = p->rchild
;若
p->rtag == 0
, 则 p 必有右孩子, 则 next = p的右子树中最左下结点;
//1. 找到以P为根的子树中,第一个被中序遍历的结点
ThreadNode *Firstnode(ThreadNode *p){
//循环找到最左下的结点(不一定是叶结点)
while(p->ltag == 0)
p=p->lchild;
return p;
}
//2. 在中序线索二叉树中找到结点p的后继结点
ThreadNode *Nextnode(ThreadNode *p){
//右子树最左下结点
if(p->rtag==0)
return Firstnode(p->rchild);
else
return p->rchild; //rtag==1,直接返回后继线索
}
//3. 对中序线索二叉树进行中序遍历
void Inorder(ThreadNode *T){
//T为根节点指针
for(ThreadNode *p = Firstnode(T); p!=NULL; p = Nextnode(p))
visit(p);
}
时间复杂度 = O(1)
中序线索二叉树找中序前驱:在中序线索二叉树中找到指定节点 *p
的中序前驱 pre
若
p->ltag == 1
, 则next = p->lchild
;若
p->ltag == 0
, 则 p 必有左孩子, 则 next = p的左子树中最右下结点;
//1. 找到以P为根的子树中,第一个被中序遍历的结点
ThreadNode *Lastnode(ThreadNode *p){
//循环找到最右下的结点(不一定是叶结点)
while(p->rtag == 0)
p=p->rchild;
return p;
}
//2. 在中序线索二叉树中找到结点p的前驱结点
ThreadNode *Prenode(ThreadNode *p){
//左子树最右下结点
if(p->ltag==0)
return Lastnode(p->lchild);
else
return p->lchild; //rtag==1,直接返回前驱线索
}
//3. 对中序线索二叉树进行逆向中序遍历
void RevInorder(ThreadNode *T){
//T为根节点指针
for(ThreadNode *p = Lastnode(T); p!=NULL; p = Prenode(p))
visit(p);
}
先序线索二叉树找先序后继:在先序线索二叉树中找到指定节点 *p
的先序后继 next
若
p->rtag == 1
, 则next = p->rchild
;若
p->rtag == 0
, 则 p 必有右孩子(左孩子不知道)case1: 若p有左孩子 ——— 根 左 右 / 根 (根 左 右) 右
case2: 若p没有左孩子 ——— 根 右 / 根 (*根 *左 右)
先序线索二叉树找先序前驱:在先序线索二叉树中找到指定节点 *p
的先序前驱pre
若
p->ltag == 1
, 则next = p->lchild
;若
p->ltag == 0
, 则 p 必有左孩子,但是先序遍历中,左右子树的结点只可能是根的后继,不可能是前驱,所以不能从左右孩子里寻找p的先序前驱,(除非从头开始遍历/三叉链表case1: 如果能够找到p的父节点,且p是左孩子 —— p的父节点就是p的前驱;
case2: 如果能够找到p的父节点,且p是右孩子,且其左兄弟为空 —— p的父节点就是p的前驱;
case3: 如果能够找到p的父节点,且p是右孩子,且其左兄弟非空 —— p的前驱为左兄弟子树中最后一个被先序遍历到的结点(根节点出发,先往右,右没有往左,找到最下一层的结点);
case4: p没有父节点,即p为根节点,则p没有先序前驱
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YjVVJjtQ-1623943811481)(先序线索二叉树找先序前驱.PNG)]
后序线索二叉树找后序前驱:在后序线索二叉树中找到指定节点 *p
的后序前驱pre
若
p->ltag == 1
, 则next = p->lchild
;若
p->ltag == 0
, 则 p 必有左孩子(不知道有没有右孩子)case1: 若p有右孩子 ——— 左 右 根 / 左 (左 右 根) 根
case2: 若p没有右孩子 ——— 左 根 (左子树按后序遍历,最后一个结点,p的左孩子)
后序线索二叉树找后序后继:在后序线索二叉树中找到指定节点 *p
的后序后继next
若
p->rtag == 1
, 则next = p->rchild
;若
p->rtag == 0
, 则 p 必有右孩子, 左孩子不知道, 但是在后序遍历中,左右子树中的结点只有可能是根的前驱,而不可能是根的后继,所以找不到后继,(除非从头开始遍历/三叉链表case1: 如果能找到p的父节点,且p是右孩子 —— p的父节点即为其后继
case2: 如果能找到p的父节点,且p是左孩子,其右兄弟为空 —— p的父节点即为其后继
case3: 如果能找到p的父节点,且p是左孩子,其右兄弟非空 —— p的后继为其右兄弟子树中第一个被后序遍历的结点;
case4: p没有父节点,即p为根节点,则p没有后序后继;
总结
中序线索二叉树 | 先序线索二叉树 | 后续线索二叉树 | |
---|---|---|---|
找前驱 | T | F | T |
找后继 | T | T | F |
#define MAX_TREE_SIZE 100 //树中最多结点数
typedef struct{
//树的结点定义
ElemType data;
int parent; //双亲位置域
}PTNode;
typedef struct{
//树的类型定义
PTNode nodes[MAX_TREE_SIZE]; //双亲表示
int n; //结点数
}PTree;
n
)-1
;②用后面的数据填补;(需要更改结点数n
)struct CTNode{
int child; //孩子结点在数组中的位置
struct CTNode *next; // 下一个孩子
};
typedef struct{
ElemType data;
struct CTNode *firstChild; // 第一个孩子
}CTBox;
typedef struct{
CTBox nodes[MAX_TREE_SIZE];
int n, r; // 结点数和根的位置
}CTree;
typedef struct CSNode{
ElemType data; //数据域
struct CSNode *firstchild, *nextsibling; //第一个孩子和右兄弟指针, *firstchild 看作左指针,*nextsibling看作右指针
}CSNode. *CSTree;
本质:森林中各个树的根节点之间视为兄弟关系
void PreOrder(TreeNode *R){
if(R!=NULL){
visit(R); //访问根节点
while(R还有下一个子树T)
PreOrder(T); //先跟遍历下一个子树
}
}
void PostOrder(TreeNode *R){
if(R!=NULL){
while(R还有下一个子树T)
PostOrder(T); //后跟遍历下一个子树
visit(R); //访问根节点
}
}
左子树结点值<跟结点值<右子树结点值
typedef struct BSTNode{
int key;
struct BSTNode *lchild, *rchild;
}BSTNode, *BSTree;
//在二叉排序树中查找值为key的结点(非递归)
//最坏空间复杂度:O(1)
BSTNode *BST_Search(BSTree T, int key){
while(T!=NULL && key!=T->key){
//若树空或等于跟结点值,则结束循环
if(key<T->key) //值小于根结点值,在左子树上查找
T = T->lchild;
else //值大于根结点值,在右子树上查找
T = T->rchild;
}
return T;
}
//在二叉排序树中查找值为key的结点(递归)
//最坏空间复杂度:O(h)
BSTNode *BSTSearch(BSTree T, int key){
if(T == NULL)
return NULL;
if(Kry == T->key)
return T;
else if(key < T->key)
return BSTSearch(T->lchild, key);
else
return BSTSearch(T->rchild, key);
}
//在二叉排序树中插入关键字为k的新结点(递归)
//最坏空间复杂度:O(h)
int BST_Insert(BSTree &T, int k){
if(T==NULL){
//原树为空,新插入的结点为根结点
T = (BSTree)malloc(sizeof(BSTNode));
T->key = k;
T->lchild = T->rchild = NULL;
return 1; //插入成功
}
else if(K == T->key) //树中存在相同关键字的结点,插入失败
return 0;
else if(k < T->key)
return BST_Insert(T->lchild,k);
else
return BST_Insert(T->rchild,k);
}
//按照str[]中的关键字序列建立二叉排序树
void Crear_BST(BSTree &T, int str[], int n){
T = NULL; //初始时T为空树
int i=0;
while(i
ASL
ASL
//平衡二叉树结点
typedef struct AVLNode{
int key; //数据域
int balance; //平衡因子
struct AVLNode *lchild; *rchild;
}AVLNode, *AVLTree;
调整最小不平衡子树
若树高为h,则最坏情况下,查找一个关键字最多需要对比h次,即查找操作的时间复杂度不可能超过O(h)
;
美国旧金山大学-可视化学习网站
void InsertSort(int A[], int n){
//A中共n个数据元素
int i,j,temp;
for(i=1; i<n; i++)
if(A[i]<A[i-1]){
//A[i]关键字小于前驱
temp = A[i];
for(j=i-1; j>=0 && A[j]>temp; --j)
A[j-1] = A[j]; //所有大于temp的元素都向后挪
A[j+1] = temp; //复制到插入位置
}
}
j>=0
void InsertSort(int A[], int n){
//A中从1开始存储,0放哨兵
int i,j;
for(i=1; i<n; i++)
if(A[i]<A[i-1]){
A[0] = A[i]; //复制为哨兵
for(j=i-1; A[0] < A[j]; --j) //从后往前查找待插入位置
A[j+1] = A[j]; //向后挪动
A[j+1] = A[0]; //复制到插入位置
}
}
思路: 先用折半查找找到应该插入的位置,再移动元素;
为了保证稳定性,当查找到和插入元素关键字一样的元素时,应该继续在这个元素的右半部分继续查找以确认位置; 即当 A[mid] == A[0]
时,应继续在mid所指位置右边寻找插入位置
当low>high
时,折半查找停止,应将[low,i-1]or[high+1,i-1]内的元素全部右移,并将A[0]复制到low所指的位置;
代码实现
void InsertSort(int A[], int n){
int i,j,low,high,mid;
for(i=2;i<=n;i++){
A[0] = A[i]; //将A[i]暂存到A[0]
low = 1; high = i-1; //折半查找的范围
while(low<=high){
//折半查找
mid = (low + high)/2; //取中间点
if(A[mid]>A[0]) //查找左半子表
high = mid - 1;
else //查找右半子表
low = mid + 1;
}
for(j=i-1; j>high+1;--j) //统一后移元素,空出插入位置
A[j+1] = A[j];
A[high+1] = A[0]
}
}
直接插入排序
相比,比较关键字的次数减少了,但是移动元素的次数没有变,时间复杂度仍然是O(n²)思路: 先追求表中元素的部分有序,再逐渐逼近全局有序;
更适用于基本有序的排序表和数据量不大的排序表,仅适用于线性表为顺序存储的情况
代码实现:
void ShellSort(ElemType A[], int n){
//A[0]为暂存单元
for(dk=n/2; dk>=1; dk=dk/2) //步长递减(看题目要求,一般是1/2
for(i=dk+1; i<=n; ++i)
if(A[i]<A[i-dk]){
A[0]=A[i];
for(j=i-dk; j>0&&A[0]<A[j];j-=dk)
A[j+dk]=A[j]; //记录后移,查找插入的位置
A[j+dk]=A[0;] //插入
}
}
**基于“交换”的排序:**根据序列中两个元素关键字的比较结果来对换这两个记录再序列中的位置;
第一趟排序使关键字值最小的一个元素“冒”到最前面(其最终位置)—— 每趟冒泡的结果是把序列中最小元素放到序列的最终位置,这样最多做n-1
趟冒泡就能把所有元素排好序;
为保证稳定性,关键字相同的元素不交换;
代码实现
//交换
void swap(int &a, int &b){
int temp = a;
a = b;
b = temp;
}
//冒泡排序
void BubbleSort(int A[], int n){
//从0开始存放
for(int i=0; i<n-1; i++){
bool flag = false; // 表示本趟冒泡是否发生交换的标志
for(int j=n-1; j>i; j--) //一趟冒泡过程
if(A[j-1]>A[j]){
//若为逆序
swap(A[j-1],A[j]); //交换
flag=true;
}
if(flag==false)
return; //本趟遍历后没有发生交换,说明表已经有序,可以结束算法
}
}
(n-1)+(n-2)+...+1 = n(n-1)/2
= 交换次数,最坏时间复杂度 = O(n²),平均时间复杂度 = O(n²)//用第一个元素将待排序序列划分为左右两个部分
int Partition(int A[], int low, int high){
int pivot = A[low]; //用第一个元素作为枢轴
while(low<high){
while(low<high && A[high]>=pivot) --high; //high所指元素大于枢轴,high左移
A[low] = A[high]; //high所指元素小于枢轴,移动到左侧
while(low<high && A[low]<=pivot) ++low; //low所指元素小于枢轴,low右移
A[high] = A[low]; //low所指元素大于枢轴,移动到右侧
}
A[low] = pivot //枢轴元素存放到最终位置
return low; //返回存放枢轴的最终位置
}
//快速排序
void QuickSort(int A[], int low, int high){
if(low<high) //递归跳出条件
int pivotpos = Partition(A, low, high); //划分
QuickSort(A, low, pivotpos - 1); //划分左子表
QuickSort(A, pivotpos + 1, high); //划分右子表
}
每一层的QuickSort
只需要处理剩余的待排序元素,时间复杂度不超过O(n);
把n个元素组织成二叉树,二叉树的层数就是递归调用的层数,n个结点的二叉树最小高度 = ⌊log₂n⌋ + 1
, 最大高度 = n
时间复杂度 = O(n×递归层数) (递归层数最大为n)
O(nlog₂n)
: 每次选的枢轴元素都能将序列划分成均匀的两部分;O(n²)
:序列本就有序或逆序,此时时间、空间复杂度最高;O(nlog₂n)
(接近最好而不是最坏)空间复杂度 = O(递归层数)(递归层数最小为log₂n)
O(log₂n)
O(n)
若每一次选中的“枢轴”可以将待排序序列划分为均匀的两个部分,则递归深度最小,算法效率最高;
若初始序列本就有序或者逆序,则快速排序的性能最差;
快速排序算法优化思路: 尽量选择可以把数据中分的枢轴元素
快速排序使所有内部排序算法中平均性能最优的排序算法;
稳定性: 不稳定;
选择排序思想: 每一趟在待排序元素中选取关键字最小(或最大)的元素加入有序子序列;
n
个元素的简单选择排序需要n-1
趟处理;//交换
void swap(int &a, int &b){
int temp = a;
a = b;
b = temp;
}
void SelectSort(int A[], int n){
//A从0开始
for(int i=0; i<n-1; i++){
//一共进行n-1趟,i指向待排序序列中第一个元素
int min = i; //记录最小元素位置
for(int j=i+1; j<n; j++) //在A[i...n-1]中选择最小的元素
if(A[j]<A[min]) min = j; //更新最小元素位置
if(min!=i)
swao(A[i],A[min]); //交换
}
}
O(1)
(n-1)+(n-2)+...+1 = n(n-2)/2
次,元素交换次数 < n-1
; 时间复杂度 = O(n²)
可理解为顺序存储的二叉树,注意
可以将堆视为一棵 完全二叉树 (✔)
可以将堆视为一棵 二叉排序树 (✖)
基本思路:每一趟在待排序元素中选取关键字最小(或最大)的元素加入有序子序列,堆顶元素的关键字最大或最小 (以下以大根堆为例)
① 将给定初始序列(n个元素),建立初始大根堆:把所有非终端结点 从后往前都检查一遍,是否满足大根堆的要求——根 ≥ 左、右
,若不满足,则将当前结点与更大的孩子互换
在顺序存储的完全二叉树中:
i≤⌊n/2⌋
i
的左孩子 2i
i
的右孩子 2i+1
i
的父节点⌊i/2⌋
更小的元素“下坠”后,可能导致下一层的子树不符合大根堆的要求,则采用相同的方法继续往下调整 —— 小元素不断“下坠”
② 基于大根堆进行排序:每一趟将堆顶元素加入有序子序列中,堆顶元素与待排序序列中最后一个元素交换后,即最大元素换到末尾,之后该位置就不用改变,即移出完全二叉树(len=len-1
),把剩下的待排序元素序列再调整为大根堆;————“一趟处理”
③ 剩下最后一个元素则不需要再调整;
//对初始序列建立大根堆
void BuildMaxHeap(int A[], int len){
for(int i=len/2; i>0; i--) //从后往前调整所有非终端结点
HeadAdjust(A, i, len);
}
/*将以k为根的子树调整为大根堆
从最底层的分支结点开始调整*/
void HeadAdjust(int A[], int k, int len){
A[0] = A[k]; //A[0]暂存子树的根结点
for(int i=2*k; i<=len; i*=2){
//沿key较大的子结点向下筛选
// i为当前所选根结点的左孩子
//i*=2是为了判断调整后再下一层是否满足大根堆
if(i<len && A[i]<A[i+1]) //判断:当前所选根结点的左、右结点哪个更大
i++; //取key较大的子结点的下标
if(A[0] >= A[i])
break; //筛选结束:i指向更大的子结点
else{
A[k] = A[i]; //将A[i]调整至双亲结点上
k=i; //修改k值,以便继续向下筛选
}
}
A[k] = A[0] //被筛选的结点的值放入最终位置
}
//交换
void swap(int &a, int &b){
int temp = a;
a = b;
b = temp;
}
//基于大根堆进行排序
void HeapSort(int A[], int len){
BuildMaxHeap(A, len); //初始建堆
for(int i=len; i>1; i--){
//n-1趟的交换和建堆过程
swap(A[i], A[1]); //堆顶元素和堆底元素交换
HeadAdjust(A,1,i-1); //把剩余的待排序元素整理成堆
}
}
归并(Merge):把两个或多个已经有序的序列合并成一个;
k路归并:每选出一个元素,需对比关键字k-1次;
外部排序通常采用归并排序,内部排序一般采用2路归并;
代码实现
//创建辅助数组B
int *B=(int *)malloc(n*sizeof(int));
//A[low,...,mid],A[mid+1,...,high] 各自有序,将这两个部分归并
void Merge(int A[], int low, int mid, int high){
int i,j,k;
for(k=low; k<=high; k++)
B[k] = A[k]; //将A中所有元素复制到B中
for(i=low, j=mid+1, k=i; i<=mid && j<= high; k++){
if(B[i]<=B[j]) //为保证稳定性两个元素相等时,优先使用靠前的那个
A[k]=B[i++]; //将较小值复制到A中
else
A[k]=B[j++];
}//for
//没有归并完的部分复制到尾部,while只会执行一个
while(i<=mid) A[k++]=B[i++]; //若第一个表未检测完,复制
while(j<=high) A[k++]=B[j++]; //若第二个表未检测完,复制
}
//递归操作
void MergeSort(int A[], int low, int high){
if(low<high){
int mid = (low+high)/2; //从中间划分
MergeSort(A, low, mid); //对左半部分归并排序
MergeSort(A, mid+1, high); //对右半部分归并排序
Merge(A,low,mid,high); //归并
}if
}
归并排序的比较次数与序列的初始状态无关;
2路归并的“归并树”——倒立的二叉树,树高h
,归并排序趟数m = h-1
,第h
层最多2^(h-1)
个结点,则满足n ≤ 2^(h-1)
,即h-1 = ⌈log₂n⌉
; 结论: n个元素进行2路归并排序,归并趟数 m = ⌈log₂n⌉
每趟归并时间复杂度为O(n)
, 算法总时间复杂度为O(nlog₂n)
;
空间复杂度为O(n)
; (归并排序算法可视为本章占用辅助空间最多的排序算法)
稳定性:归并排序是稳定的
对于N
个元素进行k
路归并排序,排序的趟数m
满足 k^m = N, m = ⌈logkN⌉
O(r)
, 其中r为基数,需要的辅助空间(队列)为r;d
趟分配收集,一趟分配需要O(n)
, 一趟收集需要O(r)
, 时间复杂度为 O[d(n+r)]
,且与序列的初始状态无关①数据元素的关键字可以方便地拆分为d组,且d较小;
②每组关键字的取值范围不大,即r较小;
③数据元素个数n较大;