算法可以总结为针对指定问题的方法论,而问题按照类型又有递归、排序、分治、动态规划等分类。
什么是递归?
递归的特点:
- 可以归纳会若干个规模较小的,与原问题形式相同的子问题。
- 临界条件。
汉诺塔问题。
分治法
分治法:将大的问题分割成可以一些可以解决的小问题,如果依然无法解决则继续分割。
可以使用分治法的问题提特征:
- 使用分治法后,难度在降低。
- 问题可分。
- 解可合并。
- 互相独立。
典型即二分法。
- 二分法查找的时间复杂度是 O(logn),这也是分治法的普遍特性。
- 二分法查找的循环次数不确定。
- 二分法处理的原问题必须是有序的。
典型题:
- 找数组 arr = [-1,2,3,4,5,6,6,8,9,10,12,15] 中第一个大于 9 的数:二分法,取中middle + (middle - 1/middle + 1) 对比。
排序
比较排序算法优劣的分析角度:
- 时间复杂度:具体包括最好时间复杂度、最坏时间复杂度以及平均时间复杂度。
- 空间复杂度:如果空间复杂度为1 ,也叫做原地排序。
- 稳定性:稳定性是指相等的数据对象,在排序之后,顺序是否保持不变。
常见的排序算法及思想:
以已下数据为例:
let a = [1, 3, 4, 2, 7, 6, 9, 8];
let b = [9, 8, 7, 6, 5, 4, 3, 2, 1];
let c = [1, 2, 3, 4, 5, 6, 7, 8, 9];
冒泡排序
function maopao(arr) {
let flag = false;
for (let i = 1; i < arr.length; i++) {
for (let j = 0; j < arr.length - i; j++) {
count++;
if (arr[j] > arr[j + 1]) {
flag = true;
let temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
if (flag === false) { //如果一次遍历后没有任何相邻的两个值是逆序的,则说明整个数组就是顺序的。无需再次执行
break;
}
}
return arr;
}
冒泡排序的复杂度:
- 对于完全顺序的数据,当添加 flag 标志后最好时间复杂度是 O(n),即只需对比一次。
- 对于完全逆序的数据,时间复杂度为O(n+(n-1)+…+1) 即 O(n*n)。
- 平均复杂度也是 O(n*n)。
- 因为不需要额外的空间,所以空间复杂度度为 O(1)。
插入排序
function charu(arr) {
for (let i = 1; i < arr.length; i++) {
let temp = arr[i]
let j = i - 1;
for (; j >= 0; j--) {
if (arr[j] > temp) {
arr[j + 1] = arr[j];
} else {
break;
}
}
arr[j+1] = temp;
}
return arr;
}
插入排序算法复杂度:
- 对于完全顺序的数据,最好时间复杂度是 O(n),即只需对比一次。
- 对于完全逆序的数据,时间复杂度为O(n+(n-1)+…+1) 即 O(n*n)。
- 平均复杂度也是 O(n*n)。
- 因为不需要额外的空间,所以空间复杂度度为 O(1)。
归并排序
function guibing(arr) {
let temp = new Array(arr.length);
console.log(`原始数据:${arr.length}`);
customMergeSort(arr, temp, 0, arr.length - 1);
console.log(`归并排序数据:${arr}`);
}
function customMergeSort(arr, temp, start, end) {
if (start < end) {
let mid = Math.floor((start + end) / 2);
//对左侧子序列进行递归排序
customMergeSort(arr, temp, start, mid);
//对右侧子序列进行递归排序
customMergeSort(arr, temp, mid + 1, end);
//合并
customDoubleMerge(arr, temp, start, mid, end);
}
}
function customDoubleMerge(arr, temp, left, mid, right) {
let p1 = left;
let p2 = mid + 1;
let k = left;
while (p1 <= mid && p2 <= right) {
count++
if (arr[p1] <= arr[p2]) {
temp[k++] = arr[p1++];
} else {
temp[k++] = arr[p2++];
}
}
while (p1 <= mid) {
count++
temp[k++] + arr[p1++];
}
while (p2 <= right) {
count++
temp[k++] = arr[p2++];
}
//复制回原数组
for (let i = left; i <= right; i++) {
arr[i] = temp[i];
}
}
归并排序算法复杂度:
- 因为采用了二分法,所以,最好、最坏、平均时间复杂度都是 O(nlogn)
- 空间复杂度上面,因为每次合并操作都要开辟基于数组的临时内存空间,所以空间复杂度为 O(n)。
- 因为归并排序设计交换操作,所以归并排序是不稳定的排序算法。
快速排序
function kuaisu(arr, start, end) {
if (start > end) {
return;
}
let i = start;
let j = end;
let temp = arr[start];
let t;
while (i < j) {
//先从右侧开始,将小于 temp 的值都放到左侧
while (temp <= arr[j] && i < j) {
j--;
}
//从左侧开始将大于 temp 的值都放到右侧。
while (temp >= arr[i] && i < j) {
i++
}
t = arr[j];
arr[j] = arr[i];
arr[i] = t;
}
arr[start] = arr[i];
arr[i] = temp;
kuaisu(arr, start, j - 1);
kuaisu(arr, j + 1, end);
}
快速排序复杂度:
- 最好的情况下都是:O(n*logn)
- 最坏的情况下则是O(n*n)。
- 平均则是 O(n*logn)。
- 空间复杂度为 O(1)。
- 因为涉及交换操作,所以快排是不稳定的排序算法。
排序算法总结
最简答的暴力排解时间复杂度是 O(n*n), 例如冒泡和插入排序。
如果利用算法思维去解决问题,使用分治法是,最好则能降低到 O(n*logn)。同时不同的算法又对空间复杂度有不同的消耗。
动态规划
- 最短路径问题。
- 最大子串问题。