一,排序
其中最基本的三种排序是冒泡,选择与插入。其平均复杂度为O(n^2). 但是其中选择排序在数据有序的情况or数据量小的情况下效率很高。虽然简单也是应该掌握的排序。
比如C++的STL库中的sort模板函数,在数据量较小的情况下使用的是插入排序,在数据量较大使用快速排序等。
1.冒泡
最简单的排序方法。思路如下 时间复杂度O(N^2) 空间复杂度O(1)
1)从左往右扫描两两比较数据,如果发现左边的数大于右边的数则交换。
第一轮扫描以后,最右边的数一定为序列中最大的数
2)第二轮开始扫描,重复步骤1,只是此次扫描排除第一轮中已经确定的最大数
3)以此类推知道最后一轮扫描仅剩下一个元素时排序结束
// Bubble sort
int bubble_sort(int *data, int length) {
int i = 0, j = 0, temp;
for(j = length - 1; j > 0; j--) {
for(i = 0; i < j; i++) {
if(data[i] > data[i + 1]) {
temp = data[i + 1];
data[i + 1] = data[i];
data[i] = temp;
}
}
}
}
2.选择排序
时间复杂度O(n^2), 空间复杂度O(1)
1)先将锚点选为第一个元素,然后逐一比较后续的每个数,如果发现有比锚点小的数则对其进行标记。
一轮扫描完以后将锚点标记的数与第一个元素进行交换;
2)接着将锚点标记为第二个数,并从第二个数起往后扫描,同样发现比锚点小的数则对齐进行重新标记。
一轮扫描完以后将锚点的数与第二个数进行交换;
3)以此类推直到只剩下最后一个数。
// Selection sort
int selection_sort(int *data, int length) {
int i = 0, j = 0, k, temp;
for(i = 0; i < length - 1; i++) {
k = i;
for(j = i + 1; j < length; j++) {
if(data[k] > data[j])
k = j;
}
// swap the data
if (k != i) {
temp = data[k];
data[k] = data[i];
data[i] = temp;
}
}
}
3.插入排序
插入排序在数据量小or基本有序的情况下其效率非常之高。最理想的时间复杂度是O(n) ,也是STL中使用的一种排序算法。
插入排序的基本思想是:
1)从第二个元素开始与其前面对比,若发现逆序则交换。直到前面的数比其小则停止扫描
2) 接着从第三个元素开始往前对比,方法与上类似。
3)直到最后一轮扫描为止结束
// Insertion sort
int insertion_sort(int *data, int length) {
int i, j, temp;
for(int i = 1; i < length; i++) {
temp = data[i];
for(j = i; j > 0 && temp < data[j - 1]; j--) {
data[j] = data[j - 1];
}
data[j] = temp;
}
}
4.希尔排序
其思想是,将数据分组。每次迭代分组的数量为 为数据长度/2 ,长度/4...并最终迭代到1组数据。
每次循环对每个组内的数据进行插入排序,这样能得到一个局部有序的子数组。最后再进行一次整体的插入排序,由于已知插入排序在数据基本有序的情况下效率最高,最优情况是O(n)的复杂度。
因此希尔排序可以认为是一种对数据基本有序情况下的优化的插入排序方法。
// Shell sort
int shell_sort(int *data, int length) {
int i = 0, j = 0, gap = 0;
int temp = 0;
//
for(gap = length / 2; gap >= 1; gap /= 2) {
for(i = gap; i < length; i++) {
temp = data[i];
for(j = i - gap; j >= 0 && temp < data[j]; j = j - gap) {
data[j + gap] = data[j];
}
data[j + gap] = temp;
}
}
return 0;
}
5. 归并排序
空间复杂度O(n),时间复杂度O(nlogn)
其思想是利用分而治之的方法,构建一个临时数组大小与原始数据空间相同。然后将原始数据用分割函数分为两部分并递归调用分割函数。
分割函数在分割递归调用返回前,将分割好的两块基本有序的数据进行合并。然后再返回上层调用,直到最后合并为整个数组。
// Merge sort
void merge(vector<int>& arr, vector<int>& temp, int start, int end) {
if (start >= end) return;
// divide
int m = start + (end - start) / 2;
merge(arr, temp, start, m);
merge(arr, temp, m + 1, end);
// conquer
int k = start, i = start, j = m + 1;
while (i <= m && j <= end) {
if (arr[i] < arr[j]) {
temp[k++] = arr[i++];
}
else {
temp[k++] = arr[j++];
}
}
while (i <= m) temp[k++] = arr[i++];
while (j <= end) temp[k++] = arr[j++];
// copy
for (i = start; i <= end; i++)
arr[i] = temp[i];
}
6. 快速排序
快速排序是最重要的排序算法,广泛应用于各种高级语言的标准库的排序算法中。例如C++的STL在非少量数据的情况下使用快速排序。
其思想也类似归并排序的分而治之。
1)快排的分割函数:
首先选择一个锚点并标记为K,然后准备两个指针分别从数组的右边和左边开始扫描。
先扫描右指针若发现右指针有小于K的数则停止
接着扫描左指针若发现有大于K的数则停止,然后将左右指针的数进行交换
最后左右指针相遇的位置就是锚点数据的位置。
2)接着将锚点数所在的位置将数组分割为左右两块并递归调用分割函数。
// Quick sort partition function.
int qsort(int *data, int left, int right) {
if (right < left) return -1;
int i = left;
int j = right;
int key = data[left];
while(i != j) {
while(i < j && key <= data[j]) j--;
data[i] = data[j];
while(i < j && key >= data[i]) i++;
data[j] = data[i];
}
data[i] = key;
qsort(data, left, i - 1);
qsort(data, i + 1, right);
return 0;
}
// quick sort
int quick_sort(int *data, int length) {
qsort(data, 0, length - 1);
}
7. 桶排序(外部排序)
待更新。。。
8. 基数排序(字典树)
待更新。。。
二,查找算法
1.二分查找
待更新。。。
2.字符串查找(KMP算法)
算法导论中KMP算法有大量的数学公式推导并且严重依赖前面的有限状态机的知识。除非是从头到尾看下来不然理解是很可能会卡壳。游管上有位大哥讲解的KMP算法通俗易懂。
https://www.bilibili.com/video/BV18k4y1m7Ar?from=search&seid=16717337984445712229
为了防止视频失效:
https://www.youtube.com/watch?reload=9&v=GTJr8OvyEVQ&list=LLe_gEtrHWO_KN1wZOpQT4
代码:
https://github.com/mission-peace/interview/blob/master/src/com/interview/string/SubstringSearch.java
提到字符串的搜索算法首先讲一下朴素搜索算法,就是从原始文本Text中遍历每个字符分别于待搜索的字符Pattern的字符一一比较,直到找到完全匹配的位置。其复杂度为O(nm)
// Naive string search algorithm.
int naiveStringSearch(const char* text, const char* pattern) {
int n = strlen(text);
int m = strlen(pattern);
if (n < m) return -1;
int i, j;
for (i = 0; i <= n - m; i++) {
for (j = 0; j < m; ++j) {
if (text[i + j] != pattern[j])
break;
}
if (j == m)
return i;
}
}
KMP算法的思路。
text: abcabcabcabcabcd
pattern: abcabcd
前缀与后缀
a --> 无
ab 前缀:a 后缀:b x 0
abc 前缀:a, ab 后缀:c, bc x 0
abca 前缀:a, ab, abc 后缀:a, ca, bca a 1
abcab 前缀:a, ab, abc, abca 后缀:b, ab, cab, bcab ab 2
abcabc 前缀: a, ab, abc, abca, abcab 后缀: c, bc, abc, cabc, bcabc abc 3
下一次对比
next数组
他利用了Pattern字符串中如果前缀与后缀存在公共长度的串,则可以减少比较次数的思路 。
void make_next(const char * pattern, int * next) {
int m = strlen(pattern);
int q, k;
next[0] = 0;
for(q = 1, k= 0; q < m; q++) {
while(k > 0 && pattern[q] != pattern[k]) {
k = next[k - 1];
}
if(pattern[q] == pattern[k]) {
k++;
}
next[q] = k;
}
}
接着使用next数组进行搜索:
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
void make_next(const char * pattern, int * next) {
int m = strlen(pattern);
int q, k;
next[0] = 0;
for(q = 1, k= 0; q < m; q++) {
while(k > 0 && pattern[q] != pattern[k]) {
k = next[k - 1];
}
if(pattern[q] == pattern[k]) {
k++;
}
next[q] = k;
}
}
int kmp(const char * text, const char * pattern, int * next) {
int n = strlen(text);
int m = strlen(pattern);
make_next(pattern, next);
int i, q; // i idx for text, q idx for pattern.
for(i = 0, q = 0; i < n; i++) {
while(q > 0 && pattern[q] != text[i]) {
q = next[q - 1];
}
if (pattern[q] == text[i])
q++;
if (q == m)
break;
}
return i - q + 1;
}
int main() {
const char * text = "abcbcglx";
const char * pattern = "bcgl";
int * next = (int*)malloc(strlen(pattern) * sizeof(int));
printf("pos %d\n", kmp(text, pattern, next));
free(next);
return 0;
}