数据结构(上) —— 线性表、栈、队列、串

当我开始这篇博客的书写时,我将知道,这是一个艰难而且繁杂的过程。一向追求实际作用的我,在学习数据分析、网络爬虫乃至自动化和前端UI等更能看得见摸得着的东西时,更能保持我的积极性和专注力。而以数据结构为代表的算法,始终无法让我静下心来,慢慢研究。我知道,算法的背后是数学,而要想让自己的编程技术上一个台阶,学习算法是必不可少的。这篇博客以程杰老师的《大话数据结构》为依托,写下我对相关数据结构的总结和学习笔记。希望对您和我都有十足的帮助。

CK出品,必属精品!

[TOC]

数据结构概述

程序设计 = 数据结构 + 算法

基本概念和术语

数据: 描述客观事物的符号,是计算机中可以操作的对象,是能被计算机识别,并输入给计算机处理的符号集合。

数据不仅仅包括整型实型等数值类型,还包括字符以及声音图像视频等非数值类型。

数据元素: 是组成数据的、有一定意义的基本单位,再计算机中通常作为整体处理。也被称为记录。

数据项: 一个数据元素可以由若干个数据项组成。数据项是数据不可分割的最小单位。

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

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

抽象数据类型(Absrtract Data Type): 是指一个数学模型及定义

1
2
3
4
5
6
7
8
9
10
11
12
ADT 抽象数据结构
Data
数据元素之间的逻辑关系定义
Operation
操作1
初始条件
操作结果描述
操作2
...
操作3
...
endADT

线性表

线性表的抽象数据类型定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
ADT 线性表(List)
Data
线性表的数据对象集合是{a, b, c, ...},每个元素的类型均为 DataType。其中,除第一个元素外,每一个元素有且只有一个直接前驱元素,除了最后一个元素外,每一个元素有且只有一个直接后驱元素。数据元素之间的关系是一对一的。
Operation
InitList(*L): 初始化操作,建立一个线性表L。
ListEmpty(L): 若线性表为空,返回 true,否则返回 false。
ClearList(*L): 将线性表清空。
GetElem(L, i, *e): 将线性表L中的第 i 个元素值返回给 e。
LocateElem(*L, e): 查找线性表中与 e 相同的元素,返回其序号,若不存在则返回 0.
Listinsert(*L, i, e): 将 e 元素插入到 L 中的第 i 个位置.
ListDelete(*L, i, *e): 删除线性表 L 中的第 i 个位置元素, 并将其值返回给 e.
ListLength(L): 返回 L 中的元素个数.
endADT

线性表的顺序存储结构 (SqList)

在算法的代码实现之前,首先声明展示这些定义和重命名,能够让我们的代码的可读性进一步提高:

1
2
3
4
5
6
7
8
#define MAXSIZE 20
#define OK 1
#define ERROR 0
#define TRUE 1
#define False 0

typedef int ElemType;
typedef int Status;

在 C语言中,存储线性表的顺序存储结构是数组,所以可以定义下面的顺序存储结构代码:

1
2
3
4
5
typedef int ElemType;
typedef struct {
ElemType data[MAXSIZE];
int length;
}SqLsit;

获取元素操作

1
2
3
4
5
6
7
8
9
10
11
// 获得数据元素
Status GetElem(SqList L, int pos, ElemType* e){
// 如果获取元素的位置小于 1 或者大于线性表长度,则返回错误
if(pos < 1 || pos > L.length){
return ERROR;
}else{
// 将第 pos - 1个元素赋给 e
*e = L.data[pos - 1];
return OK;
}
}

插入元素操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 插入元素操作
Status ListInsert(SqList * L, int pos, ElemType e){
// 如果插入元素的位置小于 1 或者大于线性表的长度 或者线性表的长度为 MAXSIZE,则返回错误
if(pos < 1 || pos > L->length+1 || L->length == MAXSIZE){
return ERROR;
}
// 若插入元素不在表尾
else if(pos <= L->length){
// 将线性表中从 length-1 到 pos-1 全部向后移动一位
for (int i = L->length-1; i >= pos-1 ; --i) {
L->data[i+1] = L->data[i];
}
}
// 将元素 e 赋给线性表中的 pos-1
L->data[pos-1] = e;
L->length ++;
return OK;
}

删除元素操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 删除元素操作
Status ListDelete(SqList * L, int pos, ElemType* e){
// 如何删除元素的位置小于 1 或者大于线性表长度,返回错误
if(pos < 1 || pos > L->length){
return ERROR;
}
// 将第 pos-1 个元素赋值给 e
*e = L->data[pos-1];
// 如果不是删除最后一个元素
if(pos < L->length){
// 将第 pos 到第 length-1 元素向前移动一位
for (int i = pos; i < L->length; ++i) {
L->data[i-1] = L->data[i];
}
}
L->length --;
return OK;
}

n个结点链结成一个链表,即为线性表的链式存储结构,因为链表的每个结点中只包含一个指针域,所以叫做单链表。

单链表的第一个结点的存储位置叫做头指针,那么整个链表的存取就必须从头指针开始进行了。最后一个链表的结点指针域为空。

Screenshot_20220107_174033_cn.wps.moffice_eng_edit_34928506304486

有时,我们为了更加方便地对链表进行操作,会在单链表的第一个结点前附设一个结点,称为头结点。头结点的数据域可以不存储任何信息,或者有时也可以存储如线性表的长度等附加信息。

头指针和头结点的异同:

头指针:

  • 是指向链表第一个结点的指针,若链表有头结点,头指针指向头结点。
  • 头指针具有标识作用,所以常用头指针冠以链表的名字
  • 无论链表是否为空,头指针均不为空。头指针是链表的必要元素

头结点:

  • 头结点是为了操作的统一和方便而设立的,放在第一元素的结点之前,其数据域一般无意义(也可存放链表的长度)
  • 有了头结点,对在第一元素结点前插入结点和删除第一结点,其操作与其它结点的操作就统一了
  • 头结点不一定是链表必须要素

综上,带有头结点的单链表可以用下图表示:

image-20220107180450886

而空的单链表可以用下图所示:

image-20220107180521384
1
2
3
4
typedef struct Node{
ElemType data;
struct Node* next;
}Node;

读取元素操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Status GetElem(LinkList L, int pos, ElemType* e){
// 定义计数器和遍历指针
int j = 1;
LinkList p = L -> next;
// 当 p 不为空并且 j < i 时工作指针后移
while(p && j < i){
p = p->next;
j++;
}
// 如果 p 为空,则返回ERROR
if(!p)
return ERROR;

*e = p->data;
return OK;
}

插入元素操作

image-20220108114106336

若想将 s 结点插入到 p 结点和 p->next 结点之间,只需要将 p->next 和 s->next 做一点改变即可。即做出如下操作:

1
2
s->next = p->next;
p->next = s;

注意:这两句代码不可交换位置

对于单链表的表头和表尾的特殊情况,操作是相同的:

image-20220108114521034
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 单链表元素的插入
Status ListInsert(LinkList *L, int pos, ElemType e){
// 定义计数器和工作指针
int j = 1;
LinkList p = *HL;
// 遍历,寻找目标位置的前一个结点
while(p && j < pos){
p = p->next;
j++;
}
// 如果 p 不存在,返回 ERROR
if (!p)
return ERROR;
LinkList s = (LinkList) malloc(sizeof(Node));
s->data = e;
s->next = p->data;
p->next = s;
return OK;
}

删除元素操作

image-20220108121244436

若想将 q 结点删除,只需要将 p->next 做一点改变即可。即做出如下操作:

1
2
3
q = p->next;
p->next = q->next;
free(q);

注意:这两句代码不可交换位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Status ListDelete(LinkList * L, int pos, ElemType* e){
// 定义计数器和工作指针
int j = 1;
LinkList p = *L;
// 遍历到目标位置的前一个结点
while (p && j < pos){
p = p->next;
j++;
}
if(!p){
return ERROR;
}
// 删除p的下一个元素
LinkList q = p->next;
p->next = q->next;
*e = q->data;
free(q);
return OK;
}

单链表的整表创建

整表创建的算法思路:

  1. 声明一结点 p 和计数器变量 i;
  2. 初始化一空链表;
  3. 让 L 的头结点指针指向 NULL,即建立一个带头结点的空链表;
  4. 循环:

头插法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 头插法创建一个空列表
void CreateListHead(LinkList *L, int n) {
LinkList p;
srand(time(0));
// 初始化头结点
*L = (LinkList) malloc(sizeof(Node));
(*L)->next = NULL;
for (int i = 0; i < n; ++i) {
// 生成新结点
p = (LinkList) malloc(sizeof (Node));
p->data = rand()%100 +1;
// 插入到头结点后
p->next = (*L)->next;
(*L)->next = p;
}
}

尾插法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 尾插法创建一个空列表
void CreateListTail(LinkList *L, int n){
LinkList p,r;
srand(time(0));
// 初始化头结点
*L = (LinkList)malloc(sizeof(Node));
(*L)->next = NULL;
// r 为指向尾部的结点
r = *L;
for (int i = 0; i < n; ++i) {
// 生成新结点
p = (LinkList) malloc(sizeof (Node));
p->data = rand()%100 +1;
r->next = p;
r = p;
}
r -> next = NULL;
}

单链表的整表删除

1
2
3
4
5
6
7
8
9
10
11
12
13
Status ClearList(LinkList * L){
LinkList p,q;
// p指向第一个结点
p = (*L) -> next;
while (p)
{
q = p->next;
free(p);
p = q;
}
(*L)->next = NULL;
return OK;
}

线性表的其他存储结构

静态链表

用数组描述的链表叫做静态链表

image-20220108142916662
1
2
3
4
5
6
7
#define MAXSIZE 1000
typedef int ElemType;
typedef struct
{
ElemType data;
int cur; // 游标(Cursor)
}Component, StaticLinkList[MAXSIZE];

双向链表

**双向链表( double linked list)是在单链表的每个结点中,再设置一个指向其前驱结点的指针域。**所以在双向链表中的结点都有两个指针域,一个指向直接后继,另一个指向直接前驱。

image-20220108145138863
1
2
3
4
5
typedef struct DulNode{
ElemType data;
struct DulNode* prior;
struct DulNode* next;
}DulNode, *DuLinkList;

双向链表在插入和删除的过程中,需要额外指定一个指针变量

插入操作:

image-20220108145450166
1
2
3
4
s->perior = p;
s->next = p->next;
p->next->perior = s;
p->next = s;

s插入顺序的关键在于先解决 s 在解决 s->next的前驱,最后解决 s->perior 的后继。

删除操作:

image-20220108145739771
1
2
3
p->prior->next = p->next;
p->next->prior = p->prior;
free(p);

循环列表

image-20220108150409510

合并循环列表:

image-20220108151109520

要想实现合并操作,只需要进行下面的步骤就可以了:

image-20220108152039671
1
2
3
4
5
p = rearA->next;
q = rearB->next;
raarA->next = q->next;
rearB->next = p;
free(q);

**栈( stack)是限定仅在表尾进行插入和删除操作的线性表。**我们把允许插入和删除的一端称为栈顶(top),另一端称为栈底( bottom),不含任何数据元素的栈称为空栈。栈又称为后进先出( Last In First Out)的线性表,简称LIFO结构。

栈的抽象数据类型定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
ADT 栈(stack
Data
同线性表。元素具有相同的类型,相邻元素具有前驱和后继的关系。
Operation
InitStack(*S): 初始化操作,建立一个空栈S。
DestroyStack(*S): 若栈存在,则销毁它。
ClearStack(*S): 将栈清空。
StackEmpty(S): 栈为空则返回 true, 否则返回 false
GetTop(S,*e): 返回栈的栈顶元素 e。
Push(*S, e): 插入元素 e 到栈顶。
Pop(*S, *e): 弹出栈顶元素,并用 e 返回其值。
StckLength(L): 返回 S 中的元素个数。
endADT

栈的顺序存储结构及其实现

1
2
3
4
typedef struct {
ElemType data[MAXSIZE];
int top;
}SqStack;

进栈操作

image-20220108154815946
1
2
3
4
5
6
7
8
9
10
11
// 将元素 e 插入栈中 ———— 进栈
Status Push(SqStack * S, ElemType e){
// 如果栈满,返回 Error
if(S->top == MAXSIZE - 1){
return ERROR;
}
// 将 top ++,并将 top + 1赋值
S->top++;
S->data[S->top] = e;
return OK;
}

出栈操作

1
2
3
4
5
6
7
8
9
Status Pop(SqStack *S, ElemType *e){
if(S->top == -1){
return ERROR;
}
// 将栈顶元素返回值给e,然后top--
*e = S->data[S->top];
S->top--;
return OK;
}

栈的复用

采用如图所示的方式,两栈各自向中间靠拢:

image-20220108155822895

存储结构:

1
2
3
4
5
typedef struct {
ElemType data[MAXSIZE];
int top1;
int top2;
}SqDoubleStack;

插入元素:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Status Push(SqDoubleStack *S, ElemType e, int StackNumber){
// 如果 top1 + 1 == top2 说明栈满
if(S->top1 == S->top2 + 1){
return ERROR;
}
if(StackNumber==1){
S->top1++;
S->data[S->top1] = e;
}else if(StackNumber == 2){
S->top2--;
S->data[S->top2] = e;
}
return OK;
}

删除元素:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Status Pup(SqDoubleStack *S, ElemType *e, int StackNumber){
if(StackNumber==1){
// 如果 top1 == 0 说明栈空
if(S->top1 == -1){
return ERROR;
}
*e = S->data[S->top1];
S->top1--;
}else if(StackNumber == 2){
// 如果 top2 == MAXSIZE 说明栈空
if(S->top1 == MAXSIZE){
return ERROR;
}
*e = S->data[S->top2];
S->top2++;
}
return OK;
}

栈的链式存储结构及其实现

链栈的结构代码:

1
2
3
4
5
6
7
8
9
typedef struct StackNode{
ElemType data;
struct StackNode* next;
}StackNode, *LinkStackPtr;

typedef struct LinkStack{
LinkStackPtr top;
int count
}LinkStack;

进栈操作

image-20220108162449416
1
2
3
4
5
6
7
8
Status Push(LinkStack * S, ElemType e){
LinkStackPtr s = (LinkStackPtr) malloc(sizeof(StackNode));
s->data = e;
s->next = S->top;
S->top = s;
S->count++;
return OK;
}

出栈操作

image-20220108162526207
1
2
3
4
5
6
7
8
9
10
11
Status Pop(LinkStack* S, ElemType*e){
if(S->count == 0){
return ERROR;
}
LinkStackPtr p = S->top;
*e = p->data;
S->top = p->next;
free(p);
S->count --;
return OK;
}

队列

**队列( queue)是只允许在一端进行插入操作,而在另一端进行删除操作的线性表。**队列是一种先进先出( First In First Out)的线性表,简称FIFO。允许插入的端称为队尾,允许删除的一端称为队头。

image-20220108165717337
1
2
3
4
5
6
7
8
9
10
11
12
13
ADT队列( Queue)
Data
同线性表。元素具有相同的类型,相邻元素具有前驱和后继关系。
Operation
Initqueue(*Q):初始化操作,建立一个空队列Q
Destroyqueue(*Q):若队列Q存在,则销毁它。
Clearqueue(E*Q):将队列Q清空。
QueueEmpty(Q):若队列Q为空,返回true,否则返回false
GetHead(Q,*e):若队列Q存在且非空,用e返回队列Q的队头元素。
Enqueue(*Q,e):若队列Q存在,插入新元素e到队列Q中并成为队尾元素。
Dequeue(*Q,*e):删除队列Q中队头元素,并用e返回其值。
Queuelength(Q):返回队列Q的元素个数
endadt

队列的顺序存储结构

image-20220108201009666

循环队列

存储结构代码:

1
2
3
4
5
typedef struct {
ElemType data[MAXSIZE];
int front; // 头指针
int rear; // 尾指针,指向队尾的下一个元素
}SqQueue;

循环队列的初始化

1
2
3
4
5
Status InitQueue(SqQueue* Q){
Q->front = 0;
Q->rear = 0;
return OK;
}

判断队列是否满

在判断是否已经满了的时候可以采用两种办法:

  • 空队列时, front等于rear,现在当队列满时,也是 front等于rear,那么如何判断此时的队列究竟是空还是满呢?
  • 办法一是设置一个标志变量 tag ,当font==rear,且tag=0时为队列空,当font==rear,且tag=1时为队列满
  • 办法二是当队列空时,条件就是 front=rear,当队列满时,我们修改其条件,保留一个元素空间。也就是说,队列满时,数组中还有一个空闲单元。

在方法二中:

image-20220108195647383

可能会遇见上图中的两种情况,则判断队列是否为满的条件是:(rear+1)%MAXSIZE == front

求队列的长度

1
2
3
4
// 求循环队列的长度
int QueueLength(SqQueue Q){
return (Q.rear - Q.front + MAXSIZE)%MAXSIZE;
}

入队

1
2
3
4
5
6
7
8
9
10
11
// 入队
Status EnQueue(SqQueue* Q, ElemType e){
// 如果队满返回 ERROR
if((Q->rear+1)%MAXSIZE == Q->front){
return ERROR;
}
Q->data[Q->rear] = e;
// 尾指针向后移动一位
Q->rear = (Q->rear+1)%MAXSIZE;
return OK;
}

出队

1
2
3
4
5
6
7
8
9
10
11
// 出队
Status DeQueue(SqQueue *Q, ElemType* e){
// 如果队空,返回 ERROR
if(Q->front == Q->rear){
return ERROR;
}
*e = Q->data[Q->front];
// 头指针向后移动一位
Q->front = (Q->front + 1)% MAXSIZE;
return OK;
}

队列的链式存储结构

image-20220108201215973

队列的链式存储结构,其实就是线性表的单链表,只不过它只能尾进头出而已,我们把它简称为链队列。

存储结构代码:

1
2
3
4
5
6
7
8
typedef struct QNode{
ElemType data;
struct QNode* next;
}QNode, *QueuePtr;

typedef struct {
QueuePtr front, rear;
}LinkQueue;

入队操作

image-20220108201745057
1
2
3
4
5
6
7
8
9
10
Status EnQueue(LinkQueue *Q, ElemType e){
// 创建 s 结点
QueuePtr s = (QueuePtr) malloc(sizeof(QNode));
s->data = e;
s->next = NULL;
// 将尾结点连上 s 结点,并将尾结点向后移动一位
Q->rear->next = s;
Q->rear = s;
return OK;
}

出队操作

image-20220108201804055
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Status DeQueue(LinkQueue* Q, ElemType* e){
// 判断是否为空
if(Q->rear == Q->front){
return ERROR;
}
// 1. 获取第一个元素结点 2. 赋值给 e 3. 将头结点连上第二个结点 4. 释放原第一元素结点空间
QueuePtr p = Q->front->next;
*e = p->data;
Q->front->next = p->next;
// 若队头是队尾,删除后,将队尾指向头结点,见上图
if(p == Q->rear){
Q->rear = Q->front;
}
free(p);
return OK;
}

串( string)是由零个或多个字符组成的有限序列,又名叫字符串。

串的抽象数据类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ADT	串(string)
Data
串中元素仅由一个字符组成,相邻元素具有前驱和后继关系。
Operation
StrAssign(T,* chars):生成一个其值等于字符串常量 chars的串T。
StrCopy(T,S):串S存在,由串S复制得串T。
ClearString(S):串S存在,将串清空。
StringEmpty(S):若串S为空,返回true,否则返回 false
StrLength(S):返回串S的元素个数,即串的长度。
StrCompare(S,T):若S>T,返回值>0,若S=T,返回0,若S<m,返回值<0
Concat(T,S1,S2):用T返回由S1和S2联接而成的新串。
Substring(Sub,S,pos,len):串S存在,用Sub返回串S的第pos个字符起长度为1en的子串。
Index(S,T,pos):串S和T存在,T是非空串,若主串S中存在和串值相同的子串,则返回它在主串S中第pos个字符之后第一次出现的位置,否则返回0
Replace(S,T,V):串S、T和V存在,T是非空串。用V替换主串S中出现的所有与T相等的不重叠的子串。
StrInsert(S,pos,T):串S和T存在,在串S的第pos个字符之前插入串T。
StrDelete(s,pos,len):串S存在,从串S中删除第pos个字符起长度为1en的子串。
ENDADT

串的比较

串的比较

给定两个串: s= a1 a2 a ns= \ {a}_{1} \ {a}_{2} \cdots \cdots \ {a}_{\ {n}} ,  t=b 1 b 2 b m\ {t}= {b}_{\ {1}} \ {b}_{\ {2}} \cdots \cdots \ {b}_{\ {m}} , 当满足以下条件之一时,  s< t\ {s}<\ {t}

  1.  n< m\ {n}<\ {m}, 且  a i= b i( i= 1,2,, n)\ {a}_{\ {i}}=\ {b}_{\ {i}}(\ {i}=\ {1}, 2, \cdots \cdots, \ {n})

例如当 s=s= “hap", t=t= “happy”, 就有 s<ts<t 。因为 ttss 多出了两个字母。

  1. 存在某个  k min( m, n)\ {k} \leqslant \ {m i n}(\ {m}, \ {n}), 使得  a i= b i\ {a}_{\ {i}}=\ {b}_{\ {i}} i= 1,2,, k 1\ {i}=\ {1}, 2, \cdots \cdots, \ {k}-\ {1} ),  a k< b k\ {a}_{\ {k}}<\ {b}_{\ {k}}

例如当 s=s= “happen”, t=t= “happy”, 因为两串的前 4 个字母均相同, 而两串第 5 个字母 ( k\mathrm{k} 值), 字母 e\mathrm{e} 的 ASCII 码是 101, 而字母 y\mathrm{y} 的 ASCII 码是 121, 显然 e<y\mathrm{e}<\mathrm{y}, 所以 s<t\mathrm{s}<\mathrm{t}

只有当两个串的长度和它们各个位置对应的字符都相等时,才算相等。

串的顺序存储结构

1
2
3
4
typedef struct{
char str[MAXSIZE];
int len;
}SqString;

串的链式存储结构

1
2
3
4
typedef struct Node{
char data;
struct Node* next;
}SNode;