这篇文章,作者将用 「 七张动图 」 来阐述一种最基础的顺序结构
「 顺序表 」
相信看我文章的大多数都是 「 大学生 」,能上大学的都是 「 精英 」,那么我们自然要 「 精益求精 」,如果你还是 「 大一 」,那么太好了,你拥有大把时间,当然你可以选择 「 刷剧 」,然而, 「 学好算法 」,三年后的你自然 「 不能同日而语 」。
那么这里,我整理了 「 几十个基础算法 」 的分类,点击开启:
《算法入门指引》
如果链接被屏蔽,或者有权限问题,可以私聊作者解决。大致题集一览:
为了让这件事情变得有趣,以及 「 照顾初学者 」,目前题目只开放最简单的算法 「 枚举系列 」 (包括:线性枚举、双指针、前缀和、二分枚举、三分枚举),当有 一半成员刷完 「 枚举系列 」 的所有题以后,会开放下个章节,等这套题全部刷完,你还在群里,那么你就会成为 「 夜深人静写算法 」专家团 的一员。
不要小看这个专家团,三年之后,你将会是别人 望尘莫及 的存在。如果要加入,可以联系我,考虑到大家都是学生, 没有「 主要经济来源 」,在你成为神的路上, 「 不会索取任何 」。
放心成神!自律使你自由!
C语言免费动漫教程,和我一起打卡! 《光天化日学C语言》
LeetCode 太难?先看简单题! 《C语言入门100例》
数据结构难?不存在的! 《画解数据结构》
闭关刷 LeetCode,剑指大厂Offer! 《算法入门指引》
LeetCode 太简单?算法学起来! 《夜深人静写算法》
说了这么多,学习之前,我们来看下 「 枚举系列 」 中几个经典的算法,将算法之前,我会介绍一下 C语言 中一种最简单的数据结构,即 数组。首先来看,内容提要:
顺序存储结构,是指用一段地址连续的存储单元依次存储线性表的数据元素。
在编程语言中,用一维数组来实现顺序存储结构,在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[i]==0? 0 : sum[l[i]-1]; // (3)
int rightsum = sum[r[i]];
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
实现了元素的交换,这里需要用&
转换成地址作为传参。 关于 「 数组的算法 」 的内容到这里就结束了。
如果还有不懂的问题,可以 「 通过主页 」找到作者的「 联系方式 」 ,线上沟通交流。
有关《画解数据结构》 的源码均开源,链接如下:《画解数据结构》
如果你满足如下:
( 1 ) (1) (1) 有强烈欲望「 想要学好C语言 」的人
( 2 ) (2) (2) 有强烈欲望「 想要学好C++ 」的人
( 3 ) (3) (3) 有强烈欲望「 想要学好数据结构 」的人
( 4 ) (4) (4) 有强烈欲望「 想学好算法 」的人
( 5 ) (5) (5) 有强烈欲望「 想进大厂 」的人
如果你满足以上任意一点,那么,我们就是志同道合的人啦!可以联系我免费加入我们团队。
饭不食,水不饮,题必须刷
C语言免费动漫教程,和我一起打卡! 《光天化日学C语言》
LeetCode 太难?先看简单题! 《C语言入门100例》
数据结构难?不存在的! 《画解数据结构》
闭关刷 LeetCode,剑指大厂Offer! 《算法入门指引》
LeetCode 太简单?算法学起来! 《夜深人静写算法》