据说「 前言 」 写太多会被人唾弃,所以,这次直接进入正题。
「 画解数据结构 」
点击我跳转末尾 获取 粉丝专属 《算法和数据结构》源码。
❤️《画解数据结构》(1-1)画解顺序表❤️
❤️《画解数据结构》(1-2)画解链表❤️
❤️《画解数据结构》(1-3)画解栈❤️
❤️《画解数据结构》(1-4)画解队列❤️
❤️《画解数据结构》(1-5)画解双端队列❤️
❤️《画解数据结构》(1-6)画解哈希表❤️
❤️《画解数据结构》(2-1)画解树❤️
❤️《画解数据结构》(2-2)画解二叉树❤️
❤️《画解数据结构》(2-3)画解二叉搜索树❤️
❤️《画解数据结构》(2-4)画解堆❤️
❤️《画解数据结构》(2-5)画解AVL树❤️
❤️《画解数据结构》(2-6)画解线段树❤️
❤️《画解数据结构》(2-7)画解字典树❤️
❤️《画解数据结构》(2-8)画解霍夫曼树❤️
❤️《画解数据结构》(2-9)画解并查集❤️
❤️《画解数据结构》(3-1)画解图❤️
❤️《画解数据结构》(3-2)画解二分匹配❤️
❤️《画解数据结构》(3-3)画解最短路❤️
❤️《画解数据结构》(3-5)画解最小生成树❤️
❤️《画解数据结构》(3-4)画解强连通❤️
顺序存储结构,是指用一段地址连续的存储单元依次存储线性表的数据元素。
在编程语言中,用一维数组来实现顺序存储结构,在C语言中,把第一个数据元素存储到下标为 0 的位置中,把第 2 个数据元素存储到下标为 1 的位置中,以此类推。
数组的长度指的是数组当前有多少个元素,数组的容量指的是数组最大能够存放多少个元素。如果数组元素大于最大能存储的范围,在程序上是不允许的,可能会产生意想不到的问题,实现上是需要规避的。
如上图所示,数组的长度为 5,即红色部分;容量为 8,即红色 加 蓝色部分。
#define MAXN 1024
#define DataType int // (1)
struct SeqList {
DataType data[MAXN]; // (2)
int length; // (3)
};
DataType
,定义为int
;SeqList
定义的就是一个最多存放MAXN
个元素的数组,MAXN
代表数组容量;length
代表数组长度,即当前的元素个数。索引 就是通过 数组下标 寻找 数组元素 的过程。C语言实现如下:
DataType SeqListIndex(struct SeqList *sq, int i) {
return sq->data[i]; // (1)
}
查找 就是通过 数组元素 寻找 数组下标 的过程,是索引的逆过程。
对于有序数组,可以采用 二分 进行查找,时间复杂度为 O ( l o g 2 n ) O(log_2n) O(log2n);对于无序数组,只能通过遍历比较,由于元素可能不在数组中,可能遍历全表,所以查找的最坏时间复杂度为 O ( n ) O(n) O(n)。
简单介绍一个线性查找的例子,实现如下:
DataType SeqListFind(struct SeqList *sq, DataType dt) {
int i;
for(i = 0; i < sq->length; ++i) {
// (1)
if(sq->data[i] == dt) {
return i; // (2)
}
}
return -1; // (3)
}
获取 数组的长度 指的是查询当前有多少元素。可以直接用结构体的内部变量。C语言代码实现如下:
DataType SeqListGetLength(struct SeqList *sq) {
return sq->length;
}
插入接口定义为:在数组的第 k k k 个元素前插入一个数 v v v。由于数组是连续存储的,那么从 k k k 个元素往后的元素都必须往后移动一位,当 k = 0 k=0 k=0 时,所有元素都必须移动,所以最坏时间复杂度为 O ( n ) O(n) O(n)。C语言代码实现如下:
int SeqListInsert(struct SeqList *sq, int k, DataType v) {
int i;
if(sq->length == MAXN) {
return 0; // (1)
}
for(i = sq->length; i > k; --i) {
sq->data[i] = sq->data[i-1]; // (2)
}
sq->data[k] = v; // (3)
sq->length ++; // (4)
return 1; // (5)
}
插入接口定义为:将数组的第 k k k 个元素删除。由于数组是连续存储的,那么第 k k k 个元素删除,往后的元素势必要往前移动一位,当 k = 0 k=0 k=0 时,所有元素都必须移动,所以最坏时间复杂度为 O ( n ) O(n) O(n)。C语言代码实现如下:
int SeqListDelete(struct SeqList *sq, int k) {
int i;
if(sq->length == 0) {
return 0; // (1)
}
for(i = k; i < sq->length - 1; ++i) {
sq->data[i] = sq->data[i+1]; // (2)
}
sq->length --; // (3)
return 1; // (4)
}
1)无须为表示表中元素逻辑关系而增加额外的存储空间;
2)随机存取元素时可以达到 O ( 1 ) O(1) O(1),效率高;
1)插入和删除时需要移动大量元素;
2)必须一开始就确定存储空间的容量;
给定一个长度为 n ( 1 ≤ n ≤ 1 0 5 ) n(1 \le n \le 10^5) n(1≤n≤105) 的整型数组,求所有数组元素中的其中的最小值。
蓝色的数据代表的是数组数据,红色的数据代表当前枚举到的数据,这样就可以遍历所有的数据进行逻辑处理了。
遍历数组,进行条件判断,条件满足则执行逻辑。这里的条件就是 枚举到的数 是否小于 当前最小值,执行逻辑为 将 当前枚举到的数 赋值给 当前最小值;
int findMin(int* nums, int numsSize){
int i, min = 100000;
for(i = 0; i < numsSize; ++i) {
// (1)
if(nums[i] < min) {
// (2)
min = nums[i];
}
}
return min; // (3)
}
min
小,则将它赋值给min
;否则,不做任何处理;min
中存储的就是整个数组的最小值。给定一个 n ( n ≤ 1 0 5 ) n (n \le 10^5) n(n≤105) 个元素的整型数组 a i a_i ai,再给出 m ( m ≤ 1 0 5 ) m(m \le 10^5) m(m≤105) 次询问,每次询问是一个区间 [ l , r ] [l, r] [l,r],求 h ( l , r ) = ∑ k = l r a k h(l,r) = \sum_{k=l}^r a_k h(l,r)=∑k=lrak
如上图所示,只需要记录一个前缀和,然后就可以通过一次减法将区间的值计算出来。时间复杂度 O ( 1 ) O(1) O(1)。这种就是差分的思想。
第一个枚举,利用一个数组sum
,存储前 i i i 个元素的和。
第二个枚举,读入 m m m 组数据 l , r l, r l,r,对每组数据,通过 O ( 1 ) O(1) O(1) 获取答案,即 s u m r − s u m l − 1 sum_r - sum_{l-1} sumr−suml−1。
int sum[maxn];
int* prefixSum(int* nums, int numsSize, int m, int *l, int *r){
int i;
int *ret;
for(i = 0; i < numsSize; ++i) {
sum[i] = nums[i];
if(i)
sum[i] += sum[i-1]; // (1)
}
ret = (int *) malloc( m * sizeof(int) ); // (2)
for(i = 0; i < m; ++i) {
int leftsum = l==0? 0 : sum[l-1]; // (3)
int rightsum = sum[r];
ret[i] = rightsum - leftsum; // (4)
}
return ret;
}
给定一个长度为 n ( 1 ≤ n ≤ 1 0 7 ) n (1 \le n \le 10^7) n(1≤n≤107) 的字符串 s s s,求一个最长的满足所有字符不重复的子串。
维护两个指针 i i i 和 j j j,区间 [ i , j ] [i, j] [i,j] 内的子串,应该时刻保持其中所有字符不重复,一旦发现重复字符,就需要自增 i i i(即执行 i = i + 1 i = i + 1 i=i+1);否则,执行 j = j + 1 j = j + 1 j=j+1,直到 j j j 不能再增加为止。
过程中,记录合法情况下 j − i + 1 j - i + 1 j−i+1 的最大值。
如上文所述,这种利用问题特性,通过两个指针,不断调整区间,从而求出问题最优解的算法就叫 “尺取法”,由于利用的是两个指针,所以又叫 “双指针” 算法。
这里 “尺” 的含义,主要还是因为这类问题,最终要求解的都是连续的序列(子串),就好比一把尺子一样,故而得名。
算法描述如下:
1)初始化 i = 0 i=0 i=0, j = i − 1 j=i-1 j=i−1,代表一开始 “尺子” 的长度为 0;
2)增加 “尺子” 的长度,即 j = j + 1 j = j +1 j=j+1;
3)判断当前这把 “尺子” [ i , j ] [i, j] [i,j] 是否满足题目给出的条件:
3.a)如果不满足,则减小 “尺子” 长度,即 i = i + 1 i = i + 1 i=i+1,回到 3);
3.b)如果满足,记录最优解,回到 2);
int getmaxlen(int n, char *str, int& l, int& r) {
int ans = 0, i = 0, j = -1, len; // 1)
memset(h, 0, sizeof(h)); // 2)
while (j++ < n - 1) {
// 3)
++h[ str[j] ]; // 4)
while (h[ str[j] ] > 1) {
// 5)
--h[ str[i] ];
++i;
}
len = j - i + 1;
if(len > ans) // 6)
ans = len, l = i, r = j;
}
return ans;
}
i = 0, j = -1
,代表 s [ i : j ] s[i:j] s[i:j] 为一个空串,从空串开始枚举;h[ str[j] ] > 1
满足时,代表出现了重复字符str[j]
,这时候左端点 i i i 推进,直到没有重复字符为止;j - i + 1
,更新;给定一个 n ( n ≤ 1 0 6 ) n(n \le 10^6) n(n≤106) 个元素的有序整型数组和一个 t a r g e t target target 值,求在 O ( l o g 2 n ) O(log_2n) O(log2n) 的时间内找到值为 t a r g e t target target 的整型的数组下标,不存在则返回 -1。
需要找值为 5 5 5 的这个元素。
黄色箭头 代表都是左区间端点 l l l,红色箭头 代表右区间端点 r r r。蓝色的数据为数组数据,绿色的数字代表的是数组下标,初始化 l = 0 l = 0 l=0, r = 7 r = 7 r=7,由于数组有序,则可以直接折半,令 m i d = ( l + r ) / 2 = 3 mid = (l + r) / 2 = 3 mid=(l+r)/2=3,则 5 5 5 一定落入区间 [ 0 , 3 ] [0, 3] [0,3],这时候令 r = 3 r = 3 r=3,继续执行,直到 l > r l > r l>r 结束迭代。
最后,当 m i d = 2 mid=2 mid=2 时,找到数据 5。
a)令初始情况下,数组下标从 0 开始,且数组长度为 n n n,则定义一个区间,它的左端点是 l = 0 l=0 l=0,右端点是 r = n − 1 r = n-1 r=n−1;
b)生成一个区间中点 m i d = ( l + r ) / 2 mid = (l + r) / 2 mid=(l+r)/2,并且判断 m i d mid mid 对应的数组元素和给定的目标值的大小关系,主要有三种:
b.1)目标值 等于 数组元素,直接返回 m i d mid mid;
b.2)目标值 大于 数组元素,则代表目标值应该出现在区间 [ m i d + 1 , r ] [mid+1, r] [mid+1,r],迭代左区间端点: l = m i d + 1 l = mid + 1 l=mid+1;
b.3)目标值 小于 数组元素,则代表目标值应该出现在区间 [ l , m i d − 1 ] [l, mid-1] [l,mid−1],迭代右区间端点: r = m i d − 1 r = mid - 1 r=mid−1;
c)如果这时候 l > r l > r l>r,则说明没有找到目标值,返回 − 1 -1 −1;否则,回到 b)继续迭代。
int search(int *nums, int numsSize, int target) {
int l = 0, r = numsSize - 1; // (1)
while(l <= r) {
// (2)
int mid = (l + r) >> 1; // (3)
if(nums[mid] == target) {
return mid; // (4)
}else if(target > nums[mid]) {
l = mid + 1; // (5)
}else if(target < nums[mid]) {
r = mid - 1; // (6)
}
}
return -1; // (7)
}
>> 1
等价于除 2,也就是这里mid
代表的是l
和r
的中点;nums[mid] == target
表示正好找到了这个数,则直接返回下标mid
;target > nums[mid]
表示target
这个数在区间 [ m i d + 1 , r ] [mid+1, r] [mid+1,r] 中,所以才有左区间赋值如下:l = mid + 1;
target < nums[mid]
表示target
这个数在区间 [ l , m i d − 1 ] [l, mid - 1] [l,mid−1] 中,所以才有右区间赋值如下:r = mid - 1;
-1
;三分枚举 类似 二分枚举 的思想,也是将区间一下子砍掉一块基本完全不可能的块,从而减小算法的时间复杂度。只不过 二分枚举 解决的是 单调性 问题。而 三分枚举 解决的是 极值问题。
给定一个 n n n 个元素的数组,数组下标从 0 0 0 开始,采用「 插入排序 」将数组按照 「升序」排列。
图示 | 含义 |
---|---|
■ 的柱形 | 代表尚未排好序的数 |
■ 的柱形 | 代表正在执行 比较 和 移动 的数 |
■ 的柱形 | 代表已经排好序的数 |
■ 的柱形 | 代表待执行插入的数 |
我们看到,首先需要将 「第二个元素」 和 「第一个元素」 进行 「比较」,如果 前者 小于等于 后者,则将 后者 进行向后 「移动」,前者 则执行插入;
然后,进行第二轮「比较」,即 「第三个元素」 和 「第二个元素」、「第一个元素」 进行 「比较」, 直到 「前三个元素」 保持有序 。
最后,经过一定轮次的「比较」 和 「移动」之后,一定可以保证所有元素都是 「升序」 排列的。
整个算法的执行过程分以下几步:
1) 循环迭代变量 i = 1 → n − 1 i = 1 \to n-1 i=1→n−1;
2) 每次迭代,令 x = a [ i ] x = a[i] x=a[i], j = i − 1 j = i-1 j=i−1,循环执行比较 x x x 和 a [ j ] a[j] a[j],如果产生 x ≤ a [ j ] x \le a[j] x≤a[j] 则执行 a [ j + 1 ] = a [ j ] a[j+1] = a[j] a[j+1]=a[j]。然后执行 j = j + 1 j = j + 1 j=j+1,继续执行 2);否则,跳出循环,回到 1)。
#include
int a[1010];
void Input(int n, int *a) {
for(int i = 0; i < n; ++i) {
scanf("%d", &a[i]);
}
}
void Output(int n, int *a) {
for(int i = 0; i < n; ++i) {
if(i)
printf(" ");
printf("%d", a[i]);
}
puts("");
}
void InsertSort(int n, int *a) {
// (1)
int i, j;
for(i = 1; i < n; ++i) {
int x = a[i]; // (2)
for(j = i-1; j >= 0; --j) {
// (3)
if(x <= a[j]) {
// (4)
a[j+1] = a[j]; // (5)
}else
break; // (6)
}
a[j+1] = x; // (7)
}
}
int main() {
int n;
while(scanf("%d", &n) != EOF) {
Input(n, a);
InsertSort(n, a);
Output(n, a);
}
return 0;
}
void InsertSort(int n, int *a)
为 插入排序 的实现,代表对a[]
数组进行升序排序。a[i]
前面的 i-1
个数都认为是排好序的,令x = a[i]
;a[j]
比需要插入的数x
大,则当前数往后挪一个位置;x
插入到合适位置;给定一个 n n n 个元素的数组,数组下标从 0 0 0 开始,采用「 选择排序 」将数组按照 「升序」排列。
图示 | 含义 |
---|---|
■ 的柱形 | 代表尚未排好序的数 |
■ 的柱形 | 代表正在执行 比较 的数 |
■ 的柱形 | 代表已经排好序的数 |
■ 的柱形 | 有两种:1、记录最小元素 2、执行交换的元素 |
我们发现,首先从 「第一个元素」 到 「最后一个元素」 中选择出一个 「最小的元素」,和 「第一个元素」 进行 「交换」;
然后,从 「第二个元素」 到 「最后一个元素」 中选择出一个 「最小的元素」,和 「第二个元素」 进行 「交换」。
最后,一定可以保证所有元素都是 「升序」 排列的。
整个算法的执行过程分以下几步:
1) 循环迭代变量 i = 0 → n − 1 i = 0 \to n-1 i=0→n−1;
2) 每次迭代,令 m i n = i min = i min=i, j = i + 1 j = i+1 j=i+1;
3) 循环执行比较 a [ j ] a[j] a[j] 和 a [ m i n ] a[min] a[min],如果产生 a [ j ] < a [ m i n ] a[j] \lt a[min] a[j]<a[min] 则执行 m i n = j min = j min=j。执行 j = j + 1 j = j + 1 j=j+1,继续执行这一步,直到 j = = n j == n j==n;
4) 交换 a [ i ] a[i] a[i] 和 a [ m i n ] a[min] a[min],回到 1)。
#include
int a[1010];
void Input(int n, int *a) {
for(int i = 0; i < n; ++i) {
scanf("%d", &a[i]);
}
}
void Output(int n, int *a) {
for(int i = 0; i < n; ++i) {
if(i)
printf(" ");
printf("%d", a[i]);
}
puts("");
}
void Swap(int *a, int *b) {
int tmp = *a;
*a = *b;
*b = tmp;
}
void SelectionSort(int n, int *a) {
// (1)
int i, j;
for(i = 0; i < n - 1; ++i) {
// (2)
int min = i; // (3)
for(j = i+1; j < n; ++j) {
// (4)
if(a[j] < a[min]) {
min = j; // (5)
}
}
Swap(&a[i], &a[min]); // (6)
}
}
int main() {
int n;
while(scanf("%d", &n) != EOF) {
Input(n, a);
SelectionSort(n, a);
Output(n, a);
}
return 0;
}
void SelectionSort(int n, int *a)
为选择排序的实现,代表对a[]
数组进行升序排序。min
代表当前第 i i i 轮迭代的最小元素的下标为 i i i。min
中。给定一个 n n n 个元素的数组,数组下标从 0 0 0 开始,采用「 冒泡排序 」将数组按照 「升序」排列。
图示 | 含义 |
---|---|
■ 的柱形 | 代表尚未排好序的数 |
■ 的柱形 | 代表正在执行比较的两个数 |
■ 的柱形 | 代表已经排好序的数 |
我们看到,首先需要将 「第一个元素」 和 「第二个元素」 进行 「比较」,如果 前者 大于 后者,则进行 「交换」,然后再比较 「第二个元素」 和 「第三个元素」 ,以此类推,直到 「最大的那个元素」 被移动到 「最后的位置」 。
然后,进行第二轮「比较」,直到 「次大的那个元素」 被移动到 「倒数第二的位置」 。
最后,经过一定轮次的「比较」 和 「交换」之后,一定可以保证所有元素都是 「升序」 排列的。
整个算法的执行过程分以下几步:
1) 循环迭代变量 i = 0 → n − 1 i = 0 \to n-1 i=0→n−1;
2) 每次迭代,令 j = i j = i j=i,循环执行比较 a [ j ] a[j] a[j] 和 a [ j + 1 ] a[j+1] a[j+1],如果产生 a [ j ] > a [ j + 1 ] a[j] \gt a[j+1] a[j]>a[j+1] 则交换两者的值。然后执行 j = j + 1 j = j + 1 j=j+1,这时候对 j j j 进行判断,如果 j ≥ n − 1 j \ge n-1 j≥n−1,则回到 1),否则继续执行 2)。
#include
int a[1010];
void Input(int n, int *a) {
for(int i = 0; i < n; ++i) {
scanf("%d", &a[i]);
}
}
void Output(int n, int *a) {
for(int i = 0; i < n; ++i) {
if(i)
printf(" ");
printf("%d", a[i]);
}
puts("");
}
void Swap(int *a, int *b) {
int tmp = *a;
*a = *b;
*b = tmp;
}
void BubbleSort(int n, int *a) {
// (1)
bool swapped;
int last = n;
do {
swapped = false; // (2)
for(int i = 0; i < last - 1; ++i) {
// (3)
if(a[i] > a[i+1]) {
// (4)
Swap(&a[i], &a[i+1]); // (5)
swapped = true; // (6)
}
}
--last;
}while (swapped);
}
int main() {
int n;
while(scanf("%d", &n) != EOF) {
Input(n, a);
BubbleSort(n, a);
Output(n, a);
}
return 0;
}
void BubbleSort(int n, int *a)
为冒泡排序的实现,代表对a[]
数组进行升序排序。swapped
标记本轮迭代下来,是否有元素产生了交换。last
的自减,所以待排序的元素会越来越少。swap
实现了元素的交换,这里需要用&
转换成地址作为传参。
链表 是由一个个 结点 组成,每个 结点 之间通过 链接关系 串联起来,每个 结点 都有一个 后继节点,最后一个 结点 的 后继结点 为 空结点。如下图所示:
A -> B
组织起来的两个结点,B
被称为A
的后继结点,A
被称为B
的前驱结点。typedef int DataType;
struct ListNode {
DataType data; // (1)
ListNode *next; // (2)
};
typedef
将它和int
同名,本文的 数据域 也会全部采用int
类型进行讲解;malloc
来创建一个 链表结点,然后对 数据域 和 指针域 进行赋值,代码实现如下:ListNode *ListCreateNode(DataType data) {
ListNode *node = (ListNode *) malloc ( sizeof(ListNode) ); // (1)
node->data = data; // (2)
node->next = NULL; // (3)
return node; // (4)
}
malloc
分配一块内存空间,用来存放ListNode
即链表结点对象;data
;首先介绍 尾插法 ,顾名思义,即 从链表尾部插入 的意思,就是记录一个 链表尾结点,然后遍历给定数组,将数组元素一个一个插到链表的尾部,每插入一个结点,则将它更新为新的 链表尾结点。注意初始情况下,链表尾结点 为空。
上图演示的是 尾插法 的整个过程,其中:
head 代表链表头结点,创建完一个结点以后,它就保持不变了;
tail 代表链表尾结点,即动图中的 绿色结点;
vtx 代表正在插入链表尾部的结点,即动图中的 橙色结点,插入完毕以后,vtx 变成 tail;
ListNode *ListCreateListByTail(int n, int a[]) {
ListNode *head, *tail, *vtx; // (1)
int idx;
if(n <= 0)
return NULL; // (2)
idx = 0;
vtx = ListCreateNode(a[0]); // (3)
head = tail = vtx; // (4)
while(++idx < n) {
// (5)
vtx = ListCreateNode(a[idx]); // (6)
tail->next = vtx; // (7)
tail = vtx; // (8)
}
return head; // (9)
}
对应的注释如下:
( 1 ) (1) (1)head
存储头结点的地址,tail
存储尾结点的地址,vtx
存储当前正在插入结点的地址;
( 2 ) (2) (2) 当需要创建的元素个数为 0 时,直接返回空链表;
( 3 ) (3) (3) 创建一个 数据域 为a[0]
的链表结点;
( 4 ) (4) (4) 由于初始情况下只有一个结点,所以将链表头结点head
和链表尾结点tail
都置为vtx
;
( 5 ) (5) (5) 从数组第 1 个元素 (0 - based) 开始,循环遍历数组;
( 6 ) (6) (6) 由于数组中第 0 个元素已经创建过了,所以这里只需要对除了第 0 个元素以外的数据创建链表结点;
( 7 ) (7) (7) 结点创建出来后,将当前链表尾结点tail
的 后继结点 置为vtx
;
( 8 ) (8) (8) 将最近创建的结点vtx
作为新的 链表尾结点;
( 9 ) (9) (9) 返回链表头结点;
头插法,顾名思义,就是每次从头结点前面进行插入,但是这样一来,就会导致插入的数据元素是 逆序 的,所以我们需要 逆序访问数组 执行插入,此所谓 负负得正 的思想。
上图所示的是 头插法 的整个插入过程,其中:
head 代表链表头结点,即动图中的 绿色结点,每新加一个结点,头结点就变成了新加入的结点;
tail 代表链表尾结点,创建完一个结点以后,它就保持不变了;
vtx 代表正在插入链表头部的结点,即动图中的 橙色结点,插入完毕以后,vtx 变成 head;
ListNode *ListCreateListByHead(int n, int *a) {
ListNode *head = NULL, *vtx; // (1)
while(n--) {
// (2)
vtx = ListCreateNode(a[n]); // (3)
vtx->next = head; // (4)
head = vtx; // (5)
}
return head; // (6)
}
对应的注释如下:
( 1 ) (1) (1)head
存储头结点的地址,初始为空链表,vtx
存储当前正在插入结点的地址;
( 2 ) (2) (2) 总共需要插入 n n n 个结点,所以采用逆序的 n n n 次循环;
( 3 ) (3) (3) 创建一个元素值为a[i]
的链表结点,注意,由于逆序,所以这里 i i i 的取值为 n − 1 → 0 n-1 \to 0 n−1→0;
( 4 ) (4) (4) 将当前创建的结点的 后继结点 置为 链表的头结点head
;
( 5 ) (5) (5) 将链表头结点head
置为vtx
;
( 6 ) (6) (6) 返回链表头结点;
ListCreateNode
在代码里出现了两次,而 头插法 只出现了一次,将流程简化了,所以还是推荐使用 头插法。那么,如何打印一个链表呢?我们可以这么思考:
链表的每个结点都有一个 后继结点 ,我们可以用A -> B
代表结点B
是结点A
的 后继结点,而对于最后一个结点而言,它的后继可以用NULL
表示。所以,我们可以循环输出所有结点并且带上->
,然后在最后加上NULL
。
void ListPrint(ListNode *head) {
ListNode *vtx = head;
while(vtx) {
// (1)
printf("%d -> ", vtx->data); // (2)
vtx = vtx->next; // (3)
}
printf("NULL\n"); // (4)
}
对应的注释如下:
( 1 ) (1) (1) 从头结点开始,循环遍历所有结点;
( 2 ) (2) (2) 遍历到的结点,将结点的 数据域 带上->
后输出;
( 3 ) (3) (3) 将 当前结点 置为 当前结点 的 后继结点,继续迭代;
( 4 ) (4) (4) 最后输出一个NULL
,代表一个完整的链表;
1 -> 3 -> 8 -> 2 -> 6 -> NULL
ListNode *ListCreateListByHead(int n, int *a) {
ListNode *head = NULL, *vtx;
while(n--) {
vtx = ListCreateNode(a[n]);
vtx->next = head;
head = vtx;
ListPrint(head); /*看这里,看这里!*/
}
return head;
}
6 -> NULL
2 -> 6 -> NULL
8 -> 2 -> 6 -> NULL
3 -> 8 -> 2 -> 6 -> NULL
1 -> 3 -> 8 -> 2 -> 6 -> NULL
验证算法的正确性有两个有效的办法:
( 1 ) (1) (1) 构造大量的 测试数据 进行输入输出测试;
( 2 ) (2) (2) 打印每一个操作后,数据结构的 当前状态,看是否和预期相符;
给定一个链表头结点
head
,并且给定一个索引值 i ( i ≥ 0 ) i (i \ge 0) i(i≥0),求这个链表的第 i i i 个结点(为了和 C语言 的数组下标保持一致,我们假定链表头结点代表第 0 个结点)。
上图演示的是通过遍历,索引到第 3 个结点(下标从 0 开始计数)的过程,其中:
head 代表链表头结点;
tail 代表链表尾结点;
j / temp 代表当前枚举到的第 j ( j ≥ 0 ) j (j \ge 0) j(j≥0)个结点,即动图中的 橙色实心结点;
ListNode *ListGetNode(ListNode *head, int i) {
ListNode *temp = head; // (1)
int j = 0; // (2)
while(temp && j < i) {
// (3)
temp = temp->next; // (4)
++j; // (5)
}
if(!temp || j > i) {
return NULL; // (6)
}
return temp; // (7)
}
temp
代表从链表头开始的 游标指针,用于对链表进行 遍历 操作;j
代表当前访问到了第 j j j 个结点;j < i
,则代表还没访问到目标结点,继续执行循环;j
自增,等价于j = j + 1;
j > i
,则说明给定的i
超过了链表长度,返回 空结点;i
个结点;void testListGetNode(ListNode *head) {
int i;
for(i = 0; i < 7; ++i) {
ListNode *node = ListGetNode(head, i);
if(!node)
printf("index(%d) is out of range.\n", i);
else
printf("node(%d) is %d.\n", i, node->data);
}
}
int main() {
int a[5] = {
1, 3, 8, 2, 6};
ListNode *head = ListCreateListByHead(5, a); // (1)
testListGetNode(head); // (2)
return 0;
}
node(0) is 1.
node(1) is 3.
node(2) is 8.
node(3) is 2.
node(4) is 6.
index(5) is out of range.
index(6) is out of range.
给定一个链表头
head
,并且给定一个值 v v v,查找出这个链表上 数据域 等于 v v v 的第一个结点。
上图演示的是通过遍历,查找到值为 2 的结点的过程,其中:
head 代表链表头结点;
tail 代表链表尾结点;
j / temp 代表当前枚举到的第 j ( j ≥ 0 ) j (j \ge 0) j(j≥0)个结点,即动图中的 橙色实心结点;
ListNode *ListFindNodeByValue(ListNode *head, DataType v) {
ListNode *temp = head; // (1)
while(temp) {
// (2)
if(temp->data == v) {
return temp; // (3)
}
temp = temp->next; // (4)
}
return NULL; // (5)
}
temp
代表从 链表头 开始遍历的 游标指针;v
相等,立即返回该结点对应的指针;void testListFindNodeByValue(ListNode *head) {
int i;
for(i = 1; i <= 6; ++i) {
ListNode *node = ListFindNodeByValue(head, i);
if(!node)
printf("value(%d) is not found!\n", i);
else
printf("value(%d) is found!\n", i);
}
}
int main() {
int a[5] = {
1, 3, 8, 2, 6};
ListNode *head = ListCreateListByHead(5, a);
testListFindNodeByValue(head);
return 0;
}
ListFindNodeByValue
去链表中查找,运行结果如下:value(1) is found!
value(2) is found!
value(3) is found!
value(4) is not found!
value(5) is not found!
value(6) is found!
给定一个链表头
head
,并且给定一个位置 i ( i ≥ 0 ) i(i \ge 0) i(i≥0) 和 一个值 v v v,求生成一个值为 v v v 的结点,并且将它插入到 链表 第 i i i 个结点之后。
上图演示的是通过遍历,将数据为 8 的结点插入到链表第 1 个(下标从 0 开始)结点后的过程,其中:
head 代表链表头结点;
tail 代表链表尾结点;
pre 代表待插入结点的 前驱结点,也是 游标指针 指代的结点,即动图中的 橙色实心结点;
aft 代表 待插入结点 的 后继结点,即动图中的 蓝色实心结点;
vtx 代表将要插入的结点,即动图中的 绿色实心结点;
ListNode *ListInsertNode(ListNode *head, int i, DataType v) {
ListNode *pre, *vtx, *aft; // (1)
int j = 0; // (2)
pre = head; // (3)
while(pre && j < i) {
// (4)
pre = pre->next; // (5)
++j; // (6)
}
if(!pre) {
return NULL; // (7)
}
vtx = ListCreateNode(v); // (8)
aft = pre->next; // (9)
vtx->next = aft; // (10)
pre->next = vtx; // (11)
return vtx; // (12)
}
pre -> vtx -> aft
;j == i
时,表明找到要插入的位置;v
的 孤立结点;vtx
插入到pre -> aft
之间,插入完毕后pre -> vtx -> aft
。void testListInsertNode(ListNode *head) {
ListPrint(head);
ListInsertNode(head, 1, 8);
ListPrint(head);
}
int main() {
int a[5] = {
1, 3, 2, 6};
ListNode *head = ListCreateListByHead(4, a);
testListInsertNode(head);
return 0;
}
1 -> 3 -> 2 -> 6
的基础上,在第 1 个结点(0 - based)的后面插入一个值为 8 的结点,并且返回这个结点。这个例子的运行结果如下:1 -> 3 -> 2 -> 6 -> NULL
执行插入操作!
1 -> 3 -> 8 -> 2 -> 6 -> NULL
给定一个链表头
head
,并且给定一个位置 i ( i ≥ 0 ) i(i \ge 0) i(i≥0),将位置为 i i i 的结点删除,并且返回新链表的头结点(为什么要返回头结点?因为被删掉的有可能是原来的头结点)。
上图演示的是通过遍历,将第 2 号结点(下标从 0 开始)删除的过程,其中:
head 代表链表头结点;
tail 代表链表尾结点;
pre 代表待删除结点的前驱结点,也是游走指针指代的结点,即动图中的 橙色实心结点;
aft 代表待删除结点的后继结点,即动图中的 绿色实心结点;
del 代表将要删除的结点,即动图中的 红色实心结点;
ListNode *ListDeleteNode(ListNode *head, int i) {
ListNode *pre, *del, *aft;
int j = 0;
if(head == NULL) {
return NULL; // (1)
}
if(i == 0) {
// (2)
del = head; // (3)
head = head->next; // (4)
free(del); // (5)
return head; // (6)
}
pre = head; // (7)
while(pre && j < i - 1) {
// (8)
pre = pre->next;
++ j;
}
if(!pre || !pre->next) {
// (9)
return head;
}
del = pre->next; // (10)
aft = del->next; // (11)
pre->next = aft; // (12)
free(del); // (13)
return head; // (14)
}
free
释放内存;pre
;del
;aft
;void testListDeleteNode(ListNode *head) {
ListPrint(head);
printf("执行 2 号结点删除操作!\n");
head = ListDeleteNode(head, 2);
ListPrint(head);
printf("执行 0 号结点删除操作!\n");
head = ListDeleteNode(head, 0);
ListPrint(head);
}
int main() {
int a[5] = {
1, 3, 8, 2, 6};
ListNode *head = ListCreateListByHead(5, a); // (1)
testListDeleteNode(head); // (2)
return 0;
}
1 -> 3 -> 8 -> 2 -> 6
的链表, 然后将 2 号结点删除,再将 头结点删除,运行结果如下:1 -> 3 -> 8 -> 2 -> 6 -> NULL
执行 2 号结点删除操作!
1 -> 3 -> 2 -> 6 -> NULL
执行 0 号结点删除操作!
3 -> 2 -> 6 -> NULL
链表的销毁,就是需要将 所有结点 的内存空间进行释放,并且需要将 链表的头结点 置空。
上图所示的是 链表销毁 的整个插入过程,其中:
head 代表链表头结点,即动图中的 绿色结点,每删除一个结点,头结点 就变成了之前头结点的 后继结点;
tail 代表链表尾结点;
temp 代表 待删除结点,即动图中的 橙色结点,执行删除后,它的内存空间就释放了;
void ListDestroyList(ListNode **pHead) {
// (1)
ListNode *head = *pHead; // (2)
while(head) {
// (3)
head = ListDeleteNode(head, 0); // (4)
}
*pHead = NULL; // (5)
}
head
才能是NULL
,否则外部会得到一个内存已经释放了的 野指针;void ListDestroyList(ListNode **pHead) {
ListNode *head = *pHead;
while(head) {
head = ListDeleteNode(head, 0);
}
*pHead = NULL;
}
void testListDestroyList(ListNode **head) {
ListPrint(*head);
ListDestroyList(head);
ListPrint(*head);
}
int main() {
int a[5] = {
1, 3, 8, 2, 6};
ListNode *head = ListCreateListByHead(5, a);
testListDestroyList(&head);
return 0;
}
head
变为NULL
。1 -> 3 -> 8 -> 2 -> 6 -> NULL
3 -> 8 -> 2 -> 6 -> NULL
8 -> 2 -> 6 -> NULL
2 -> 6 -> NULL
6 -> NULL
NULL
NULL
内存分配:
由于是链式存储,随时增加元素随时分配内存,不需要像数组那样进行预分配存储空间;
插入:
当拥有链表某个结点的指针时,在它 后继位置 插入一个新的结点的的时间复杂度为 O ( 1 ) O(1) O(1);
删除:
当拥有链表某个结点的指针时,删除它的 后继结点 的时间复杂度为 O ( 1 ) O(1) O(1);
索引:
索引第几个结点时,时间复杂度为 O ( n ) O(n) O(n);
查找:
查找是否存在某个结点时,时间复杂度为 O ( n ) O(n) O(n);
栈 是仅限在 表尾 进行 插入 和 删除 的 线性表。
栈 又被称为 后进先出 (Last In First Out) 的线性表,简称 LIFO 。
栈 是一个线性表,我们把允许 插入 和 删除 的一端称为 栈顶。
和 栈顶 相对,另一端称为 栈底,实际上,栈底的元素我们不需要关心。
栈的插入操作,叫做 入栈,也可称为 进栈、压栈。如下图所示,代表了三次入栈操作:
栈的删除操作,叫做 出栈,也可称为 弹栈。如下图所示,代表了两次出栈操作:
一直 出栈,直到栈为空,如下图所示:
对于一个栈来说只能获取 栈顶 数据,一般不支持获取 其它数据。
栈元素个数一般用一个额外变量存储,入栈 时加一,出栈 时减一。这样获取栈元素的时候就不需要遍历整个栈。通过 O ( 1 ) O(1) O(1) 的时间复杂度获取栈元素个数。
当栈元素个数为零时,就是一个空栈,空栈不允许 出栈 操作。
对于顺序表,在 C语言 中表现为 数组,在进行 栈的定义 之前,我们需要考虑以下几个点:
1)栈数据的存储方式,以及栈数据的数据类型;
2)栈的大小;
3)栈顶指针;
#define DataType int // (1)
#define maxn 100005 // (2)
struct Stack {
// (3)
DataType data[maxn]; // (4)
int top; // (5)
};
DataType
这个宏定义来统一代表栈中数据的类型,这里将它定义为整型,根据需要可以定义成其它类型,例如浮点型、字符型、结构体 等等;maxn
代表我们定义的栈的最大元素个数;Stack
就是我们接下来会用到的 栈结构体;DataType data[maxn]
作为栈元素的存储方式,数据类型为DataType
,可以自行定制;top
即栈顶指针,data[top-1]
表示栈顶元素,top == 0
代表空栈;如图所示,蓝色元素 为原本在栈中的元素,红色元素 为当前需要 入栈 的元素,执行完毕以后,栈顶指针加一。具体来看下代码实现。
void StackPushStack(struct Stack *stk, DataType dt) {
// (1)
stk->data[ stk->top ] = dt; // (2)
stk->top = stk->top + 1; // (3)
}
stk
是一个指向栈对象的指针,由于这个接口会修改栈对象的成员变量,所以这里必须传指针,否则,就会导致函数执行完毕,传参对象没有任何改变;stk->top < maxn
,void StackPushStack(struct Stack *stk, DataType dt) {
stk->data[ stk->top++ ] = dt;
}
stk->top++
表达式的值是自增前的值,并且自身进行了一次自增。如图所示,蓝色元素 为原本在栈中的元素,红色元素 为当前需要 出栈 的元素,执行完毕以后,栈顶的指针减一。具体来看下代码实现。
void StackPopStack(struct Stack* stk) {
--stk->top;
}
如图所示,对于数组来说,清空栈的操作只需要将 栈顶指针 置为栈底,也就是数组下标 0 即可,下次继续 入栈 的时候会将之前的内存重复利用。
void StackClear(struct Stack* stk) {
stk->top = 0;
}
DataType StackGetTop(struct Stack* stk) {
return stk->data[ stk->top - 1 ]; // (1)
}
int StackGetSize(struct Stack* stk) {
return stk->top; // (2)
}
bool StackIsEmpty(struct Stack* stk) {
return !StackGetSize(stk); // (3)
}
/************************************* 栈的顺序表实现 *************************************/
#define DataType int
#define bool int
#define maxn 100010
struct Stack {
DataType data[maxn];
int top;
};
void StackClear(struct Stack* stk) {
stk->top = 0;
}
void StackPushStack(struct Stack *stk, DataType dt) {
stk->data[ stk->top++ ] = dt;
}
void StackPopStack(struct Stack* stk) {
--stk->top;
}
DataType StackGetTop(struct Stack* stk) {
return stk->data[ stk->top - 1 ];
}
int StackGetSize(struct Stack* stk) {
return stk->top;
}
bool StackIsEmpty(struct Stack* stk) {
return !StackGetSize(stk);
}
/************************************* 栈的顺序表实现 *************************************/
对于链表,在进行 栈的定义 之前,我们需要考虑以下几个点:
1)栈数据的存储方式,以及栈数据的数据类型;
2)栈的大小;
3)栈顶指针;
typedef int DataType; // (1)
struct StackNode; // (2)
struct StackNode {
// (3)
DataType data;
struct StackNode *next;
};
struct Stack {
struct StackNode *top; // (4)
int size; // (5)
};
struct StackNode;
是对链表结点的声明;DataType data
代表 数据域;struct StackNode *next
代表 指针域;top
作为 栈顶指针,当栈为空的时候,top == NULL
;否则,永远指向栈顶;size
来代表现在栈中有多少元素。每次 入栈时size
自增,出栈时size
自减。这样在询问栈的大小的时候,就可以通过 O ( 1 ) O(1) O(1) 的时间复杂度。如图所示,head 为栈顶,tail 为栈底,vtx 为当前需要 入栈 的元素,即图中的 橙色结点。入栈 操作完成后,栈顶 元素变为 vtx,即图中 绿色结点。
void StackPushStack(struct Stack *stk, DataType dt) {
struct StackNode *insertNode = (struct StackNode *) malloc( sizeof(struct StackNode) ); // (1)
insertNode->next = stk->top; // (2)
insertNode->data = dt; // (3)
stk->top = insertNode; // (4)
++ stk->size; // (5)
}
malloc
生成一个链表结点insertNode
;insertNode
的 后继结点;insertNode
的 数据域 设置为传参 dt
;insertNode
作为 新的栈顶;如图所示,head 为栈顶,tail 为栈底,temp 为当前需要 出栈 的元素,即图中的 橙色结点。出栈 操作完成后,栈顶 元素变为之前 head 的 后继结点,即图中 绿色结点。
void StackPopStack(struct Stack* stk) {
struct StackNode *temp = stk->top; // (1)
stk->top = temp->next; // (2)
free(temp); // (3)
--stk->size; // (4)
}
temp
中;清空栈 可以理解为,不断的出栈,直到栈元素个数为零。
void StackClear(struct Stack* stk) {
while(!StackIsEmpty(stk)) {
// (1)
StackPopStack(stk); // (2)
}
stk->top = NULL; // (3)
}
DataType StackGetTop(struct Stack* stk) {
return stk->top->data; // (1)
}
int StackGetSize(struct Stack* stk) {
return stk->size; // (2)
}
int StackIsEmpty(struct Stack* stk) {
return !StackGetSize(stk);
}
stk->top
作为 栈顶指针,它的 数据域 data
就是 栈顶元素的值,返回即可;size
记录的是 栈元素个数;/************************************* 栈的链表实现 *************************************/
typedef int DataType;
struct StackNode;
struct StackNode {
DataType data;
struct StackNode *next;
};
struct Stack {
struct StackNode *top;
int size;
};
void StackPushStack(struct Stack *stk, DataType dt) {
struct StackNode *insertNode = (struct StackNode *) malloc( sizeof(struct StackNode) );
insertNode->next = stk->top;
insertNode->data = dt;
stk->top = insertNode;
++ stk->size;
}
void StackPopStack(struct Stack* stk) {
struct StackNode *temp = stk->top;
stk->top = temp->next;
--stk->size;
free(temp);
}
DataType StackGetTop(struct Stack* stk) {
return stk->top->data;
}
int StackGetSize(struct Stack* stk) {
return stk->size;
}
int StackIsEmpty(struct Stack* stk) {
return !StackGetSize(stk);
}
void StackClear(struct Stack* stk) {
while(!StackIsEmpty(stk)) {
StackPopStack(stk);
}
stk->top = NULL;
stk->size = 0;
}
/************************************* 栈的链表实现 *************************************/
在利用顺序表实现栈时,入栈 和 出栈 的常数时间复杂度低,且 清空栈 操作相比 链表实现 能做到 O ( 1 ) O(1) O(1),唯一的不足之处是:需要预先申请好空间,而且当空间不够时,需要进行扩容,扩容方式本文未提及,可以参考以下文章:《C/C++ 面试 100 例》(四)vector 扩容策略。
在利用链表实现栈时,入栈 和 出栈 的常数时间复杂度略高,主要是每插入一个栈元素都需要申请空间,每删除一个栈元素都需要释放空间,且 清空栈 操作是 O ( n ) O(n) O(n) 的,直接将 栈顶指针 置空会导致内存泄漏。好处就是:不需要预先分配空间,且在内存允许范围内,可以一直 入栈,没有顺序表的限制。
队列 是仅限在 一端 进行 插入,另一端 进行 删除 的 线性表。
队列 又被称为 先进先出 (First In First Out) 的线性表,简称 FIFO 。
允许进行元素删除的一端称为 队首。如下图所示:
允许进行元素插入的一端称为 队尾。如下图所示:
队列的插入操作,叫做 入队。它是将 数据元素 从 队尾 进行插入的过程,如图所示,表示的是 插入 两个数据(绿色 和 蓝色)的过程:
队列的删除操作,叫做 出队。它是将 队首 元素进行删除的过程,如图所示,表示的是 依次 删除 两个数据(红色 和 橙色)的过程:
队列的清空操作,就是一直 出队,直到队列为空的过程,当 队首 和 队尾 重合时,就代表队尾为空了,如图所示:
对于一个队列来说只能获取 队首 数据,一般不支持获取 其它数据。
队列元素个数一般用一个额外变量存储,入队 时加一,出队 时减一。这样获取队列元素的时候就不需要遍历整个队列。通过 O ( 1 ) O(1) O(1) 的时间复杂度获取队列元素个数。
当队列元素个数为零时,就是一个 空队,空队 不允许 出队 操作。
对于顺序表,在 C语言 中表现为 数组,在进行 队列的定义 之前,我们需要考虑以下几个点:
1)队列数据的存储方式,以及队列数据的数据类型;
2)队列的大小;
3)队首指针;
4)队尾指针;
#define DataType int // (1)
#define maxn 100005 // (2)
struct Queue {
// (3)
DataType data[maxn]; // (4)
int head, tail; // (5)
};
DataType
这个宏定义来统一代表队列中数据的类型,这里将它定义为整型,根据需要可以定义成其它类型,例如浮点型、字符型、结构体 等等;maxn
代表我们定义的队列的最大元素个数;Queue
就是我们接下来会用到的 队列结构体;DataType data[maxn]
作为队列元素的存储方式,即 数组,数据类型为DataType
,可以自行定制;head
即队首指针,tail
即队尾指针,head == tail
代表空队;当队列非空时,data[head]
代表了队首元素(而队尾元素是不需要关心的);如图所示,绿色元素 为新插入队尾的数据,执行完毕以后,队尾指针加一,队首指针不变。需要注意的是,顺序表实现时,队尾指针指向的位置是没有数据的,具体来看下代码实现。
void QueueEnqueue(struct Queue *que, DataType dt) {
// (1)
que->data[ que->tail ] = dt; // (2)
que->tail = que->tail + 1; // (3)
}
que
是一个指向队列对象的指针,由于这个接口会修改队列对象的成员变量,所以这里必须传指针,否则,就会导致函数执行完毕,传参对象没有任何改变;que->tail < maxn
,void QueueEnqueue(struct Queue *que, DataType dt) {
que->data[ que->tail++ ] = dt;
}
que->tail++
表达式的值是自增前的值,并且自身进行了一次自增。如图所示,橙色元素 为原先的 队首元素,执行 出队 操作以后,黃色元素 成为当前的 队首元素,执行完毕以后,队首指针加一。由于是顺序表实现,队首元素前面的那些元素已经变成无效的了,具体来看下代码实现。
void QueueDequeue(struct Queue* que) {
++que->head;
}
如图所示,对于数组来说,清空队列的操作只需要将 队首指针 和 队尾指针 都置零 即可,数据不需要清理,下次继续 入队 的时候会将之前的内存重复利用。
void QueueClear(struct Queue* que) {
que->head = que->tail = 0;
}
DataType QueueGetFront(struct Queue* que) {
return que->data[ que->head ]; // (1)
}
int QueueGetSize(struct Queue* que) {
return que->tail - que->head; // (2)
}
int QueueIsEmpty(struct Queue* que) {
return !QueueGetSize(que); // (3)
}
que->head
代表了 队首指针,即 队首下标,所以真正的 队首元素 是 que->data[ que->head ]
;/**************************** 顺序表 实现队列 ****************************/
#define DataType int
#define maxn 100005
struct Queue {
DataType data[maxn];
int head, tail;
};
void QueueClear(struct Queue* que) {
que->head = que->tail = 0;
}
void QueueEnqueue(struct Queue *que, DataType dt) {
que->data[ que->tail++ ] = dt;
}
void QueueDequeue(struct Queue* que) {
++que->head;
}
DataType QueueGetFront(struct Queue* que) {
return que->data[ que->head ];
}
int QueueGetSize(struct Queue* que) {
return que->tail - que->head;
}
int QueueIsEmpty(struct Queue* que) {
return !QueueGetSize(que);
}
/**************************** 顺序表 实现队列 ****************************/
对于链表,在进行 队列的定义 之前,我们需要考虑以下几个点:
1)队列数据的存储方式,以及队列数据的数据类型;
2)队列的大小;
3)队首指针;
4)队尾指针;
typedef int DataType; // (1)
struct QueueNode; // (2)
struct QueueNode {
// (3)
DataType data;
struct QueueNode *next;
};
struct Queue {
struct QueueNode *head, *tail; // (4)
int size; // (5)
};
struct QueueNode;
是对 链表结点 的声明;DataType data
代表 数据域;struct QueueNode *next
代表 指针域;head
作为 队首指针,tail
作为 队尾指针;size
来代表现在队列中有多少元素。每次 入队时size
自增,出队时size
自减。这样在询问 队列 的大小的时候,就可以通过 O ( 1 ) O(1) O(1) 的时间复杂度。如图所示,head 为 队首元素,tail 为 队尾元素,vtx 为当前需要 入队 的元素,即图中的 橙色结点。入队 操作完成后,队尾元素 变为 vtx,即图中 绿色结点。
void QueueEnqueue(struct Queue *que, DataType dt) {
struct QueueNode *insertNode = (struct QueueNode *) malloc( sizeof(struct QueueNode) );
insertNode->data = dt; // (1)
insertNode->next = NULL;
if(que->tail) {
// (2)
que->tail->next = insertNode;
que->tail = insertNode;
}else {
que->head = que->tail = insertNode; // (3)
}
++que->size; // (4)
}
malloc
生成一个链表结点insertNode
,并且填充 数据域 和 指针域;insertNode
作为 队尾 的 后继结点,并且更新insertNode
作为新的 后继结点;insertNode
;如图所示,head 为 队首元素,tail 为 队尾元素,temp 为当前需要 出队 的元素,即图中的 橙色结点。出队 操作完成后,队首元素 变为之前 head 的 后继结点,即图中 绿色结点。
void QueueDequeue(struct Queue* que) {
struct QueueNode *temp = que->head; // (1)
que->head = temp->next; // (2)
free(temp); // (3)
--que->size; // (4)
if(que->size == 0) {
// (5)
que->tail = NULL;
}
}
temp
中;清空队列 可以理解为:不断的 出队,直到 队列元素 个数为零为止。由于链表结点是动态申请的内存,所以在没有其它结点引用时,是需要释放内存的,不像数组那样直接将 队首指针 和 队尾指针 置空就行的。
void QueueClear(struct Queue* que) {
while(!QueueIsEmpty(que)) {
// (1)
QueueDequeue(que); // (2)
}
}
DataType QueueGetFront(struct Queue* que) {
return que->head->data; // (1)
}
int QueueGetSize(struct Queue* que) {
return que->size; // (2)
}
int QueueIsEmpty(struct Queue* que) {
return !QueueGetSize(que); // (3)
}
que->head
作为 队首指针,它的 数据域 data
就是 队首元素的值,返回即可;size
记录的是 队列元素 的个数;/**************************** 链表 实现队列 ****************************/
typedef int DataType;
struct QueueNode;
struct QueueNode {
DataType data;
struct QueueNode *next;
};
struct Queue {
struct QueueNode *head, *tail;
int size;
};
void QueueEnqueue(struct Queue *que, DataType dt) {
struct QueueNode *insertNode = (struct QueueNode *) malloc( sizeof(struct QueueNode) );
insertNode->data = dt;
insertNode->next = NULL;
if(que->tail) {
que->tail->next = insertNode;
que->tail = insertNode;
}else {
que->head = que->tail = insertNode;
}
++que->size;
}
void QueueDequeue(struct Queue* que) {
struct QueueNode *temp = que->head;
que->head = temp->next;
free(temp);
--que->size;
if(que->size == 0) {
que->tail = NULL;
}
}
DataType QueueGetFront(struct Queue* que) {
return que->head->data;
}
int QueueGetSize(struct Queue* que) {
return que->size;
}
int QueueIsEmpty(struct Queue* que) {
return !QueueGetSize(que);
}
void QueueClear(struct Queue* que) {
que->head = que->tail = NULL;
que->size = 0;
}
/**************************** 链表 实现队列 ****************************/
在利用顺序表实现队列时,入队 和 出队 的常数时间复杂度低,且 清空队列 操作相比 链表实现 能做到 O ( 1 ) O(1) O(1),唯一的不足之处是:需要预先申请好空间,而且当空间不够时,需要进行扩容,扩容方式本文未提及,可以参考以下文章:《C/C++ 面试 100 例》(四)vector 扩容策略。
当然,可以采用 循环队列,能够很大程度上避免扩容问题,但是当 入队速度 大于 出队速度 时,不免还是会遇到扩容的问题。
在利用链表实现队列时,入队 和 出队 的常数时间复杂度略高,主要是每插入一个队列元素都需要申请空间,每删除一个队列元素都需要释放空间,且 清空队列 操作是 O ( n ) O(n) O(n) 的,直接将 队首指针 和 队尾指针 置空会导致内存泄漏。
好处就是:不需要预先分配空间,且在内存允许范围内,可以一直 入队,没有顺序表的限制。当然,链表的实现明显比数组实现要复杂,编码的时候容易出错。
需要注意的是,本文在讲解过程中,顺序表实现 的 队尾 和 链表实现 的 队尾 不是一个概念,顺序表实现的队尾没有实际元素值,而链表实现的则不然,请自行加以区分。
本文已超五万字,为了增加阅读体验,更多内容请收看:画解双端队列。
本文已超五万字,为了增加阅读体验,更多内容请收看:画解哈希表。
本文已超五万字,为了增加阅读体验,更多内容请收看:画解树。
本文已超五万字,为了增加阅读体验,更多内容请收看:画解二叉树。
本文已超五万字,为了增加阅读体验,更多内容请收看:画解二叉搜索树。
本文已超五万字,为了增加阅读体验,更多内容请收看:画解堆。
本文已超五万字,为了增加阅读体验,更多内容请收看:画解二叉平衡树。
本文已超五万字,为了增加阅读体验,更多内容请收看:画解线段树。
本文已超五万字,为了增加阅读体验,更多内容请收看:画解字典树。
本文已超五万字,为了增加阅读体验,更多内容请收看:画解霍夫曼树。
本文已超五万字,为了增加阅读体验,更多内容请收看:画解并查集。
本文已超五万字,为了增加阅读体验,更多内容请收看:画解图。
本文已超五万字,为了增加阅读体验,更多内容请收看:画解二分匹配。
本文已超五万字,为了增加阅读体验,更多内容请收看:画解最短路。
本文已超五万字,为了增加阅读体验,更多内容请收看:画解最小生成树。
本文已超五万字,为了增加阅读体验,更多内容请收看:画解强连通。
有关《画解数据结构》 的源码均开源,链接如下:《画解数据结构》
相信看我文章的大多数都是「 大学生 」,能上大学的都是「 精英 」,那么我们自然要「 精益求精 」,如果你还是「 大一 」,那么太好了,你拥有大把时间,当然你可以选择「 刷剧 」,然而,「 学好算法 」,三年后的你自然「 不能同日而语 」。
那么这里,我整理了「 几十个基础算法 」 的分类,点击开启:
为了让这件事情变得有趣,以及「 照顾初学者 」,目前题目只开放最简单的算法 「 枚举系列 」 (包括:线性枚举、双指针、前缀和、二分枚举、三分枚举),当有 一半成员刷完 「 枚举系列 」 的所有题以后,会开放下个章节,等这套题全部刷完,你还在群里,那么你就会成为「 夜深人静写算法 」专家团 的一员。
不要小看这个专家团,三年之后,你将会是别人 望尘莫及 的存在。如果要加入,可以联系我,考虑到大家都是学生, 没有「 主要经济来源 」,在你成为神的路上,「 不会索取任何 」。
让天下没有难学的算法
C语言免费动漫教程,和我一起打卡! 《光天化日学C语言》
入门级C语言真题汇总 《C语言入门100例》
几张动图学会一种数据结构 《画解数据结构》
组团学习,抱团生长 《算法入门指引》
竞赛选手金典图文教程 《夜深人静写算法》
语言入门:《光天化日学C语言》(示例代码)
语言训练:《C语言入门100例》试用版
数据结构:《画解数据结构》源码
算法入门:《算法入门》指引
算法进阶:《夜深人静写算法》算法模板