算法概述
时间复杂度
空间复杂度
数据结构
基础数据结构
数组
队列和栈
链表
集合
字典和散列表
树
图
排序算法
1.冒泡排序
数组有多少个元素,就需要遍历多少次数组。然后在每次遍历中,依次拿相邻两个元素的值进行比较,如果前值 > 后值,则交换量值位置,然后继续向后比较,直到数组结束。
冒泡排序的改进在于,减少内循环比较的次数,如:
[3, 2, 5, 4, 1]
在经历两轮冒泡后可以得到:
[2, 3, 1, 4, 5]
此时后两个元素顺序必然已经排序,因为每轮冒泡都会将最大的那个泡泡移动到最后,所以后两个元素就不用参与内循环了,因此可将内循环的次数 - 冒泡的轮数,即外循环的次数。
function bubbleSort(arr) {
let length = arr.length;
for (let i=0; i< length; i++) {
for (let j=0; j< length-1-i; j++) {
if (arr[j] > arr[j+1]) {
[arr[j], arr[j+1]] = [arr[j+1], arr[j]];
}
}
}
}
2.选择排序
每次找出本轮最小值并放置在第一位,再找出第二小值并放在第二位
function selectSort(arr) {
let length = arr.length;
for (let i=0; i<length; i++) {
let minIndex = i;
for (let j=i, j<length; j++) {
if (arr[minIndex] > arr[j]) {
minIndex = j; // 始终把最小的值得索引赋值给 minIndex
}
}
// 判断是否需要交换最小值到前方
if (minIndex !== i) {
[arr[i], arr[minIndex]] = [arr[minIndex], arr[i]];
}
}
}
3.插入排序
插入排序每次排一个数组项,以此方式构建最后的排序数组。假定第一项已经排序了,接着,它和第二项进行比较,第二项是应该待在原位还是插到第一项之前呢?这样,头两项就已正确排序,接着和第三项比较(它是该插入到第一、第二还是第三的位置呢?),以此类推。
function insertSort(arr) {
let length = arr.length;
for(let i=1; i<length; i++) {
let j=i;
let temp = arr[j];
while(j>0 && arr[j-1] > temp){
arr[j] = arr[j-1]; // 每次将待插入值与其前面所有元素比较,如果前面元素较大,则他们位置依次向后移动,以便给待插入值腾个地方
j--;
}
arr[j] = temp;
}
}
4.希尔排序
5.归并排序
冒泡、选择、插入三种排序的基本排序算法性能会比较差,一般不会用于实际编码中,而归并排序的复杂度为 O(nlogn),因此可以在实际中使用。
JavaScript 中 Array.sort() 排序函数使用的排序算法,一般是归并排序或快速排序.
归并排序也是用到了分治的思想.
归并排序,将原始数组切分成较小的数组,直到每个数组都只有一个元素,然后再将小数组归并成大数组,直到最后只有一个排序完毕的大数组.
function mergeSort(arr) {
let length = arr.length;
if (length === 0) {
return arr;
}
index = Math.floor(length / 2);
let leftArray = arr.slice(0, index);
let rightArray = arr.slice(index, length);
merge(mergeSort(leftArray), mergeSort(rightArray));
}
function merge(left, right) {
let il = 0;
let ir = 0;
let array = [];
while(il < left.length && ir < right.length) {
if (left[il] < right[ir]) {
array.push(left[il++]);
} else {
array.push(right[ir++]);
}
}
while(il < left.length) {
array.push(left[il++]);
}
while(ir < right.length) {
array.push(right[ir++]);
}
return array;
}
6.快速排序
快速排序也属于交换排序,通过元素之间的比较和交换位置来达到排序的目的。
快速排序是设定基准值,并在每次循环事将小于基准值的元素移动到基准值左边,大于基准值的元素移动到基准值右边,从而把数组拆分成两个部分。
这种思路叫做 分治法
(1) 首先,从数组中选择中间一项作为主元。
(2) 然后,创建两个指针,左指针指向数组第一项,右指针指向数组末尾项;
移动左指针,直到该指针值大于等于主元时,接着移动右指针,直到找该指针值小于等于主元;
然后交换两指针的值,并将左右指针 ++/--;
重复(2)这个过程,直到左指针超过了右指针。
这个过程将使得比主元小的值都排在主元之前,而比主元大的值都排在主元之后,这一步叫作划分操作。
(3) 接着,算法对划分后的小数组(较主元小的值组成的子数组,以及较主元大的值组成的子数组)重复之前的两个步骤,直至数组已完全排序。
(function(){
let arr = [2,8,1,4,7,9,0,3];
quick(arr, 0, arr.length - 1);
console.log('arr:', arr);
})();
// 排序函数
function quick(arr, left, right) {
let index;
if (arr.length > 1){
index = partition(arr, left, right);
if (left < index - 1) {
quick(arr, left, index - 1);
}
if (right > index) {
quick(arr, index, right);
}
}
}
// partition 函数
function partition(arr, left, right) {
// 选取基准元素
let p = arr[math.floor((right - left) / 2)];
let i = left;
let j = right;
while(i <= j) {
while(arr[i] < p) {
i++;
}
while(arr[j] > p) {
j--;
}
if (i <= j) {
[arr[i], arr[j]] = [arr[j], arr[i]];
i++;
j--;
}
}
return i;
}
7.堆排序
8.计数排序
9.桶排序
10.基数排序
搜索算法
顺序搜索
二分搜索
算法常见模式
递归
分治
- 二分搜索
- 大整数乘法
- Strassen矩阵乘法
- 棋盘覆盖
- 合并排序
- 快速排序
- 线性时间选择
- 最接近点对问题
- 循环赛日程表
- 汉诺塔
分治法是把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题……直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。
分治法所能解决的问题一般具有以下几个特征:
1)、 该问题的规模缩小到一定的程度就可以容易地解决;
2)、 该问题可以分解为若干个规模较小的相同问题,即该问题具有最优子结构性质;
3)、 利用该问题分解出的子问题的解可以合并为该问题的解;
4)、该问题所分解出的各个子问题是相互独立的,即子问题之间不包含公共的子子问题。
动态规划
- 背包问题
- 最长公共子串
动态规划是一种在数学和计算机科学中使用的,用于求解包含重叠子问题的最优化问题的方法。其基本思想是,将原问题分解为相似的子问题,在求解的过程中通过子问题的解求出原问题的解。动态规划的思想是多种算法的基础,被广泛应用于计算机科学和工程领域。
动态规划方法通常用来求解最优化问题,这类问题可以有很多可行解,每个解都有一个值,找到具有最优值的解称为问题的一个最优解,而不是最优解,可能有多个解都达到最优值。
设计动态规划算法的步骤:
1)、刻画一个最优解的结构特征
2)、递归地定义最优解的值
3)、计算最优解的值,通常采用自底向上的方法
4)、利用算出的信息构造一个最优解
动态规划与分治法相似,都是组合子问题的解来解决原问题的解,与分治法的不同在于:分治法的子问题是相互独立存在的,而动态规划应用于子问题重叠的情况。
贪心算法
-
教室调度问题
-
背包问题
-
集合覆盖问题
-
NP 完全问题
-
贪心算法,是指在对问题求解时,总是做出当前看来是最好的选择。也就是说,不从整体最优上加以考虑,他所做出的的是在某种意义上的局部最优解。
-
贪心算法并不保证是得到最优解,但在某些问题上贪心算法的解就是最优解。要会判断一个问题能否用贪心算法来计算。
局部最优解,比一定就代表是全局最优解。
背包问题
分为 0-1 背包问题和分数背包问题
0-1 背包问题不能用贪心算法。但是分数背包问题可以用。
回溯算法
回溯法(探索与回溯法)是一种选优搜索法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。
其基本思想是,在包含问题的所有解的解空间树中,按照深度优先搜索的策略,从根结点出发深度探索解空间树。当探索到某一结点时,要先判断该结点是否包含问题的解,如果包含,就从该结点出发继续探索下去,如果该结点不包含问题的解,则逐层向其祖先结点回溯。
分支界限法
分枝界限法是一个用途十分广泛的算法,运用这种算法的技巧性很强,不同类型的问题解法也各不相同。
分支定界法的基本思想是对有约束条件的最优化问题的所有可行解(数目有限)空间进行搜索。该算法在具体执行时,把全部可行的解空间不断分割为越来越小的子集(称为分支),并为每个子集内的解的值计算一个下界或上界(称为定界)。在每次分支后,对凡是界限超出已知可行解值那些子集不再做进一步分支,这样,解的许多子集(即搜索树上的许多结点)就可以不予考虑了,从而缩小了搜索范围。这一过程一直进行到找出可行解为止,该可行解的值不大于任何子集的界限。因此这种算法一般可以求得最优解。