一、线性表的抽象数据型
1.1、线性表数据结构
线性表【Linear List】是由元素组成的有序集合。记作
(
a
0
,
.
.
.
,
a
i
−
1
,
a
i
,
a
i
+
1
,
.
.
.
,
a
n
−
1
)
(a_0, ..., a_{i-1}, a_i, a_{i+1}, ..., a_{n-1})
(a0,...,ai−1,ai,ai+1,...,an−1)其中:
-
n
n
n是有穷自然数,是线性表的长度,即线性表是有限的,当
n
=
0
n = 0
n=0时,线性表为空表,记作
(
)
()
();
-
a
i
a_i
ai是线性表的元素,
i
i
i是元素
a
i
a_i
ai的索引,元素本身的结构与线性表的结构无关;
-对于线性表的元素,
a
i
+
1
a_{i+1}
ai+1是
a
i
a_i
ai的前驱,
a
i
a_i
ai是
a
i
+
1
a_{i+1}
ai+1的后继,即线性表是有序的。
线性表的数学模型记为
L
I
S
T
=
(
D
,
R
)
D
=
{
a
i
∣
a
i
∈
E
l
e
m
e
n
t
S
e
t
,
i
=
0
,
1
,
.
.
.
,
n
−
1
,
n
≥
0
}
R
=
{
H
}
H
=
{
<
a
i
−
1
,
a
i
>
∣
a
i
−
1
,
a
i
∈
D
,
i
=
0
,
1
,
.
.
.
,
n
−
1
}
LIST = (D, R)\\ D = \{a_i | a_i \in ElementSet, i = 0, 1, ..., n - 1, n \ge 0\} \\ R = \{H\} \\ H = \{<a_{i-1}, a_i>|a_{i-1}, a_i \in D, i = 0, 1, ..., n-1\}
LIST=(D,R)D={ai∣ai∈ElementSet,i=0,1,...,n−1,n≥0}R={H}H={<ai−1,ai>∣ai−1,ai∈D,i=0,1,...,n−1}
1.2 线性表的数组描述
在线性表的数组描述中,用数组来储存线性表的元素。
假定使用一个一维数组
a
r
r
a
y
array
array来存储线性表的元素,数组的每个位置可以存储线性表的一个元素。那么需要一个映射,使线性表的一个元素对应数组的一个位置,形如
l
o
c
a
t
i
o
n
(
i
)
=
i
location(i) = i
location(i)=i 线性表的数组描述的类型定义为
struct LIST{
elementtype elements[maxlength];
int last;
};
其中定义了线性表的最后元素的索引,即 l o c a t i o n ( n − 1 ) = l a s t location(n - 1) = last location(n−1)=last。
1.3 线性表的链式描述
线性表的链式描述中,每一个元素用一个结点来描述,每一个结点都明确包含另一个相关结点的位置信息,这个位置信息称为链。
对线性表的一个可能的链式描述中,每个元素都在一个单独的结点中描述,每一个结点都有一个链域,其值式线性表的下一个元素的位置,即地址,最后一个结点的链域值为NULL,这种结构称为单向链表。
线性表的链式描述的类型定义为
struct celltype{
elementtype element;
celltype *next;
};
typedef celltype *LIST;
此外,双向链表、环形链表也可以作为线性表的描述形式。
可以将线性表的数组描述与链式描述结合起来,即线性表的元素无须有序的存放在数组的单元中,而每个单元不仅存放元素本身,而且还要存放其后继元素所在的数组单元的下标,其类型定义为
typedef int cursor;
typedef struct{
elementtype element;
int next;
} spacestr;
spacestr SPACE[maxlength];
cursor av;
1.4 广义表
广义表是线性表的一种推广结构,线性表要求其元素是相同的元素类型,而广义表中的元素可以取不同类型,包括不可再分割的元素以及广义表本身。由于广义表的元素特性,广义表具有递归性。
广义表作为结点,其包括指向下一结点的指针、储存的是元素还是指针的标志位,以及根据标志被解释为不同结构的联合体,其储存结构为
struct listnode{
listnode *link;
boolean tag;
union {
char data;
listnode *dlink;
};
};
typedef listnode *listpointer;
二、栈
2.1 栈数据结构
栈【Stack】是一种特殊的线性表,其插入和删除操作都在表的同一端进行。栈的操作端称为栈顶,而另一端称为栈底,栈的插入与删除分别称为压栈和弹栈。由于栈仅限在栈顶操作,形成了后入先出【Last In First Out,LIFO】的特性,称为LIFO结构。
2.2 栈的数组描述
栈的数组描述基于线性表的数组描述,并额外定义了栈顶元素的索引,其类型定义为
typedef struct{
elementtype elements[maxlength];
int top;
} Stack;
2.3 栈的链式描述
当使用链表描述栈时,使用链表的左端作为栈顶的性能更优,其类型定义为
struct node{
elementtype val;
node *next;
};
typedef node *Stack;
三、队列
3.1 队列数据结构
队列【Queue】是一种特殊的线性表,其插入和删除操作分别在表的不同端进行。插入元素端称为队尾,而删除元素端称为队首,队列的插入与删除分别称为入队与出队。由于队列在队列首位操作,形成了先入先出【First In First Out,FIFO】的特性,称为FIFO结构。
3.2 队列的数组描述
队列的数组描述基于线性表的数组描述,并额外定义了队首与队尾的索引,其类型定义为
struct Queue{
elementtype element[maxlength];
int front;
int rear;
};
随着元素的不断入队与出队,队列在数组上的位置会发生变化,直到队列的队尾到达了数组的末端,而数组的前面部分位置空闲,形成了假溢出的情况。
解决假溢出的方法有多种,可以不断移动元素位置,或者采用循环队列的方法。要注意的是,在循环队列中,空队列与满队列均出现队首索引与队尾索引一致的情况,需要增加标志位或指针偏置。
3.3 队列的链式描述
队列的链式描述基于线性表的链式描述,且队列需要有两个关键结点,即队首与队尾,其类型定义为
struct celltype{
elementtype element;
celltype *next;
};
struct Queue{
celltype *front;
celltype *rear;
};
四、串
4.1 串数据结构
串【String】是一种特殊的线性表,其每个元素的类型为字符型。记作
S
=
′
a
0
,
a
1
,
.
.
.
a
n
−
1
′
S = 'a_0, a_1, ... a_{n-1}\ '
S=′a0,a1,...an−1 ′其中:
-
n
n
n是串的长度,当
n
=
0
n = 0
n=0时,串为空串,记作
′
′
'\ '
′ ′;
-
a
i
a_i
ai是字符型,
i
i
i是元素
a
i
a_i
ai的索引,且在不为空串时有
0
≤
i
≤
n
−
1
0 \le i \le n - 1
0≤i≤n−1。
4.2 串的数组描述
串的数组描述采用数组的形式,自第一个元素开始依次存储字符串中的每一个字符,即
char str[20] = "Data Structures";
4.3 串的链式描述
串的链式描述基于线性表的链式描述,其结点元素为字符或字符串,其类型定义为
struct node{
char data;
node *link;
};
typedef node *STRING;
4.4 BF算法
考虑在主串
S
S
S中寻找模式串
T
T
T,称为模式匹配。简单的模式匹配称为暴力【Brute Force】算法,步骤为:
1.初始化
i
=
0
i = 0
i=0,
j
=
0
j = 0
j=0;
2.匹配元素
s
i
s_{i}
si与
t
j
t_{j}
tj;
3.若元素匹配成功,则令
i
+
=
1
i += 1
i+=1,
j
+
=
1
j += 1
j+=1并迭代步骤2,直到元素匹配失败,令
i
=
i
−
j
+
1
i = i - j + 1
i=i−j+1,
j
=
0
j = 0
j=0,迭代2,直到模式匹配成功或模式匹配结束。
令
S
S
S为主串,
T
T
T为模式,且串的第0索引存放串长度,其C实现如下
int BF(char* S, char* T, int pos){
int i = pos;
int j = 1;
while (i <= S[0] && j <= T[0]){
if (S[i] == T[j]){
i += 1;
j += 1;
}else{
i = i - j + 2;
j = 1;
}
}
if (j > T[0])
return i - T[0];
else
return 0;
}
考虑
char S[16] = "_Data_Structures";
S[0] = 15;
char T[7] = "_Struct";
T[0] = 6;
printf("match postion:%d", BF(&S, &T, 1));
可以得到结果
match postion:6
即 S [ i + 6 ] = T [ i ] , i = 1 , 2 , . . . , 6 S[i + 6] = T[i], i = 1, 2, ..., 6 S[i+6]=T[i],i=1,2,...,6。
设主串
S
S
S长为
n
n
n,模式串
S
S
S长为
m
m
m,那么可能匹配成功的
S
S
S的位置有
{
1
,
2
,
.
.
.
,
n
−
m
+
1
}
\{1, 2, ..., n - m + 1 \}
{1,2,...,n−m+1}。
最好情况下,模式匹配失败均由第一个元素匹配失败产生,那么
S
i
S_i
Si模式匹配成功前共发生了
i
−
1
i - 1
i−1次元素匹配失败,
S
i
S_i
Si模式匹配成功发生了
m
m
m次元素匹配成功,在等概率匹配成功的平均匹配次数为
∑
i
=
1
n
−
m
+
1
p
i
(
i
−
1
+
m
)
=
1
n
−
m
+
1
∑
i
=
1
n
−
m
+
1
(
i
−
1
+
m
)
=
(
m
+
n
)
/
2
\sum_{i = 1}^{n - m + 1}p_i(i - 1 + m) = \frac{1}{n - m + 1}\sum_{i = 1}^{n - m + 1}(i - 1 + m) = (m + n)/2
i=1∑n−m+1pi(i−1+m)=n−m+11i=1∑n−m+1(i−1+m)=(m+n)/2即最好情况下算法的平均时间复杂度为
O
(
n
+
m
)
O(n + m)
O(n+m)。
最坏情况下,模式匹配失败均由最后一个元素匹配失败产生,那么
S
i
S_i
Si模式匹配成功前共发生了
(
i
−
1
)
m
(i - 1)m
(i−1)m次元素匹配失败,
S
i
S_i
Si模式匹配成功发生了
m
m
m次元素匹配成功,在等概率匹配成功的平均匹配次数为
∑
i
=
1
n
−
m
+
1
p
i
(
i
m
)
=
m
n
−
m
+
1
∑
i
=
1
n
−
m
+
1
i
=
m
(
n
−
m
+
2
)
/
2
\sum_{i = 1}^{n - m + 1}p_i(im) = \frac{m}{n - m + 1}\sum_{i = 1}^{n - m + 1}i = m(n - m + 2)/2
i=1∑n−m+1pi(im)=n−m+1mi=1∑n−m+1i=m(n−m+2)/2即在
n
>
>
m
n >> m
n>>m的情况下,最坏情况下算法的平均时间复杂度为
O
(
n
m
)
O(nm)
O(nm)。
4.5 KMP算法
BF算法在每次元素匹配失败后都进行了回溯,没有利用已经部分匹配的结果。KMP【Knuth-Morris-Pratt】算法作为改进的模式匹配算法,利用已经得到的部分匹配的结果将元素匹配位置向右滑动尽可能远的一段距离,继续进行比较。
考虑主串
S
=
s
1
s
2
.
.
.
s
n
S = s_1s_2...s_n
S=s1s2...sn,而模式串
T
=
p
1
p
2
.
.
.
p
m
T = p_1p_2...p_m
T=p1p2...pm。设
s
i
−
j
+
1
s_{i-j+1}
si−j+1与
p
1
p_1
p1元素匹配并持续,直到
s
i
≠
p
j
s_i \ne p_j
si=pj,此时已知
s
i
−
j
+
1
=
p
1
s
i
−
j
+
2
=
p
2
.
.
.
s
i
−
1
=
p
j
−
1
s_{i-j+1} = p_1 \\ s_{i-j+2} = p_2 \\ ... \\s_{i-1} = p_{j-1}
si−j+1=p1si−j+2=p2...si−1=pj−1由于模式匹配失败,则匹配位置向右滑动,直到又有
s
i
−
k
+
1
=
p
1
s_{i-k+1} = p_1
si−k+1=p1并持续到
s
i
−
1
s_{i - 1}
si−1与
p
k
−
1
p_{k-1}
pk−1依然元素匹配。此时必然有
s
i
−
k
+
1
=
p
1
s
i
−
k
+
2
=
p
2
.
.
.
s
i
−
1
=
p
j
−
1
s_{i-k+1} = p_1 \\ s_{i-k+2} = p_2 \\ ... \\s_{i-1} = p_{j-1}
si−k+1=p1si−k+2=p2...si−1=pj−1那么有
p
1
=
s
i
−
j
+
1
=
s
i
−
k
+
1
p_1 = s_{i - j + 1} = s_{i - k + 1}
p1=si−j+1=si−k+1。在匹配位置向右滑动的过程中,
s
i
−
j
+
2
,
s
i
−
j
+
3
.
.
.
,
s
i
−
k
+
1
s_{i-j+2}, s_{i-j+3} ..., s_{i-k+1}
si−j+2,si−j+3...,si−k+1将逐次与
p
1
p_1
p1相比较,而根据已经得到的匹配结果,有
p
2
,
p
3
.
.
.
,
p
i
−
k
+
1
p_2, p_3..., p_{i-k+1}
p2,p3...,pi−k+1逐次与
p
1
p_1
p1相比较,若
p
l
≠
p
1
,
1
<
l
<
i
−
k
+
1
p_l \ne p_1, 1<l<i-k+1
pl=p1,1<l<i−k+1,那么元素匹配必然失败,直到
p
i
−
k
+
1
=
p
1
p_{i-k+1} = p_1
pi−k+1=p1,再进行逐一的元素匹配。这便是利用已经得到的部分匹配的结果可以自然得出的结论,当
T
T
T的某些元素与其前缀不符时,便可以直接向前滑动。
再考察滑动的情况,考虑
p
1
.
.
.
p
k
−
1
=
p
j
−
(
k
−
1
)
p
j
−
(
k
−
2
)
.
.
.
p
j
−
1
p_1...p_{k-1} = p_{j-(k-1)}p_{j-(k-2)} ... p_{j-1}
p1...pk−1=pj−(k−1)pj−(k−2)...pj−1,其中
j
j
j是某次模式匹配失败时该元素在模式串中的索引,
k
k
k是下一次模式匹配时元素匹配开始的索引,则有
k
=
n
e
x
t
[
j
]
=
{
0
,
j
=
1
m
a
x
{
k
∣
1
<
k
<
j
,
T
1
.
.
.
T
k
−
1
=
T
j
−
(
k
−
1
)
.
.
.
T
j
−
1
}
1
,
o
t
h
e
r
s
k = next[j] = \left\{\begin{aligned} &0, &&j = 1 \\ &max\{k|1<k<j, T_1...T_{k-1} = T_{j-(k-1)}...T_{j-1}\}\\ &1, &&others\\ \end{aligned}\right.
k=next[j]=⎩⎪⎨⎪⎧0,max{k∣1<k<j,T1...Tk−1=Tj−(k−1)...Tj−1}1,j=1others并给出了KMP算法的实现步骤:
1.初始化主串
S
S
S的位置
i
=
1
i = 1
i=1与模式串
T
T
T的位置
j
=
1
j = 1
j=1;
2.匹配元素
S
[
i
]
S[i]
S[i]与
T
[
j
]
T[j]
T[j];
3.如果匹配成功,令
i
+
=
1
i += 1
i+=1,
j
+
=
1
j += 1
j+=1并迭代步骤2,直到元素匹配失败,令
j
=
n
e
x
t
[
j
]
j = next[j]
j=next[j];
4.如果
j
<
1
j < 1
j<1,那么令
i
+
=
1
i += 1
i+=1,
j
+
=
1
j += 1
j+=1;
5.并迭代2-4,直到模式匹配成功或模式匹配结束。
令
S
S
S为主串,
T
T
T为模式,且串的第0索引存放串长度,其C实现如下
void get_next(char* T, int next[]){
int i = 1;
int j = 0;
next[i] = 0;
while (i < T[0]){
if (j == 0 || T[i] == T[j]){
i++;
j++;
next[i] = j;
}else
j = next[j];
}
}
int KMP(char* S, char* T, int pos, int next[]){
int i = pos;
int j = 1;
while(i <= S[0] && j <= T[0]){
if (S[i] == T[j]){
i += 1;
j += 1;
}else{
j = next[j];
if (j <= 1){
i += 1;
j += 1;
}
}
}
if (j > T[0]){
return i - T[0];
}else{
return -1;
}
}
考虑
char T[9] = "_abaabcac";
T[0] = 8;
int next[9] = {0};
get_next(&T, next);
char S[14] = "_acaaaabaabcac";
S[0] = 13;
int kmp = KMP(&S, &T, 1, next);
printf("kmp match postion:%d", kmp);
可以得到结果
kmp match postion:6
五、数组
5.1 数组数据结构
数组是由索引与值组成的数对的集合,其中任意两个数对的索引都不相同。数组在内存中采用一组连续的地址空间存储,使用索引,也称下标访问。
数组是一种特殊形式的线性表,多维数组可以被理解为一维数组,其每个元素又是一个数组。
5.2 数组的顺序储存
数组的顺序表示指的是在计算机中,用一组连续的存储单元实现数组的存储,包括主行映射与主列映射,其中,C使用主行映射按行储存数组,形如
A
=
(
A
[
0
]
[
0
]
A
[
0
]
[
1
]
A
[
0
]
[
2
]
A
[
1
]
[
0
]
A
[
1
]
[
1
]
A
[
1
]
[
2
]
)
\bm{A} = \left( \begin{matrix}\bm{A}[0][0] & \bm{A}[0][1] & \bm{A}[0][2] \\ \bm{A}[1][0] & \bm{A}[1][1] & \bm{A}[1][2] \end{matrix} \right )
A=(A[0][0]A[1][0]A[0][1]A[1][1]A[0][2]A[1][2])其在内存中的储存顺序为
{
A
[
0
]
[
0
]
,
A
[
0
]
[
1
]
,
A
[
0
]
[
2
]
,
A
[
1
]
[
0
]
,
A
[
1
]
[
1
]
,
A
[
1
]
[
2
]
}
\{\bm{A}[0][0], \bm{A}[0][1], \bm{A}[0][2], \bm{A}[1][0], \bm{A}[1][1], \bm{A}[1][2]\}
{A[0][0],A[0][1],A[0][2],A[1][0],A[1][1],A[1][2]}。对于m行n列的数组,A[i][j]的访问地址为
a
d
d
r
(
A
[
i
]
[
j
]
)
=
a
d
d
r
(
A
[
0
]
[
0
]
)
+
(
i
n
+
j
)
c
addr(\bm{A}[i][j]) = addr(\bm{A}[0][0]) + (in + j)c
addr(A[i][j])=addr(A[0][0])+(in+j)c其中,
c
c
c表示数组元素所占的地址字节数。
5.3 矩阵的压缩储存
一些常用的特殊矩阵包括对角矩阵、三角矩阵、对称矩阵、稀疏矩阵等。此类矩阵由于某些特性可以被压缩存储,基本思想为为多个值相同的元素分配一个存储空间,为0元素不分配存储空间。
在对称矩阵中,仅存储下三角部分的元素,那么将矩阵展开为一维数组时,A[i][j]的索引为
k
=
i
(
i
+
1
)
/
2
+
j
k = i(i+1)/2 + j
k=i(i+1)/2+j 在稀疏矩阵中,使用三元组表存储稀疏矩阵,将稀疏矩阵的非零元素表示为行、列、值得三元组,形如
typedef struct{
int i, j;
elementtype v;
} Triple;
typedef struct{
Triple data[maxlength];
int mu, nu, tu;
} TSMatrix;
5.3 数组的链式存储
多维数组可以使用链式,将每个数对描述为一个结点,并指向相邻的结点,同时储存行、列、值信息,形如
struct node{
node *LEFT, *UP;
int r, c;
elementtype v;
};