第一篇算法笔记
一、导读
本文示例代码使用:JavaScript
本文基础数据结构:JavaScript Array
1、排序定义
- 对一序列对象根据某种策略进行排序。
2、术语说明
- 时间复杂度: 一个算法执行所耗费的时间。
- 空间复杂度: 运行完一个程序所需内存的大小。
3、排序分类
4、复杂度小结
算法名称 | 最佳时间复杂度 | 平均时间复杂度 | 最差时间复杂度 | 空间复杂度 |
---|---|---|---|---|
冒泡排序 | O(n) | O(n2) | O(n2) | O(1) |
选择排序 | O(n2) | O(n2) | O(n2) | O(1) |
插入排序 | O(n) | O(n2) | O(n2) | O(1) |
希尔排序 | O(n) | O(nlogn) | O(nlogn) | O(1) |
快速排序 | O(nlogn) | O(nlogn) | O(n2) | O(nlogn) |
归并排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(n) |
堆排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(1) |
桶排序 | O(n+k) | O(n+k) | O(n2) | O(n+k) |
计数排序 | O(n+k) | O(n+k) | O(n+k) | O(n+k) |
基数排序 | O(n * k) | O(n * k) | O(n * k) | O(n+k) |
二、基于比较的简单排序
冒泡排序、选择排序、插入排序,三者都是基于比较的排序原理,实现都非常易懂。所以笔者在复习排序算法时,都将这三者归为一类。
1、冒泡排序:
算法原理
(1)它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。
(2)直到重复地进行直到没有再需要交换,也就是说该数列已经排序完成。
代码实现
/**
* @desc 冒泡排序
* @sort 升序排序
* @param {Array} arr
* @return {Array}
*/
function bubble ( arr ) {
// 检验:非数组、空数组直接结束函数调用。
if (Object.prototype.toString.call(arr) !== '[object Array]' || arr.length === 0) {
return [];
}
if (arr.length === 1) {
return arr;
};
let len = arr.length;
// 外层循环负责控制排序的趟数
for (let i = 0; i < len - 1; i++) {
// 内层循环负责执行一趟排序
for (let j = 0; j < len - 1 - i; j++) {
if (arr[j + 1] < arr[j]) {
[arr[j + 1], arr[j]] = [arr[j], arr[j + 1]];
};
};
};
return arr;
};
算法分析
最佳时间复杂度:O(n)
平均时间复杂度:O(n2)
最差时间复杂度:O(n2)
空间复杂度:O(1)
2、选择排序
算法原理
(1)将数组划分为已排序和未排序两部分,将未排序的元素最小元素一个个取出然后按顺序放置已排序的尾部。
(2)重复进行步骤1,直到未排序部分清空。
代码实现
/**
* @desc 选择排序
* @sort 升序排序
* @param {Array} arr
* @return {Array}
*/
function select( arr ) {
// 检验:非数组、空数组直接结束函数调用。
if (Object.prototype.toString.call(arr) !== '[object Array]' || arr.length === 0) {
return [];
}
if (arr.length === 1) {
return arr;
};
let len = arr.length;
// 外层循环需要进行 len - 1 趟排序
for (let i = 0; i < len - 1; i++) {
let min = i;
// 内层循环从未排序的数组元素中比较出最小的一个
for (let j = i + 1; j < len; j++) {
if (arr[min] > arr[j]) {
min = j;
};
};
// 将其放在排序后的数组元素的最后
[arr[min], arr[i]] = [arr[i], arr[min]];
};
return arr;
};
算法分析
最佳时间复杂度:O(n2)
平均时间复杂度:O(n2)
最差时间复杂度:O(n2)
空间复杂度:O(1)
3、插入排序
算法原理
(1)它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,通过比较,找到相应位置并插入。
代码实现
/**
* @desc 插入排序
* @sort 升序排序
* @param {Array} arr
* @return {Array}
*/
function insert ( arr ) {
// 检验:非数组、空数组直接结束函数调用。
if (Object.prototype.toString.call(arr) !== '[object Array]' || arr.length === 0) {
return [];
}
if (arr.length === 1) {
return arr;
};
let len = arr.length;
for (let i = 1; i < len; i++) {
let tmp = arr[i];
let j;
for (j = i - 1; j >= 0; j--) {
if (tmp < arr[j]) {
arr[j+1] = arr[j];
} else {
break;
};
};
// 插入取出的数组元素
arr[j+1] = tmp;
};
return arr;
}
算法分析
最佳时间复杂度:O(n)
平均时间复杂度:O(n2)
最坏时间复杂度:O(n2)
空间复杂度:O(1)
三、基于比较的复杂排序
希尔排序、快速排序、归并排序、堆排序,都是基于比较的排序原理,但实现略有复杂。所以笔者在复习排序算法时,都将这三者归为一类。
1、希尔排序
算法原理:插入排序的改良版本
(1)希尔排序是按一定步长(间隔)将待排序数组分组,对每组使用直接插入排序算法排序;
(2)随着步长逐渐减少,当步长减至1时,排序终止。
代码实现:
/**
* @desc 插入排序
* @sort 升序排序
* @param {Array} arr
* @param {Number} gap
* @return {Array}
*/
function insert (arr, gap) {
// 检验:非数组、空数组直接结束函数调用。
if (Object.prototype.toString.call(arr) !== '[object Array]' || arr.length === 0) {
return [];
}
if (arr.length === 1) {
return arr;
};
let len = arr.length;
for (let i = gap; i < len; i++) {
let tmp = arr[i];
for (var j = i - gap; j >= 0; j -= gap) {
if (tmp < arr[j]) {
arr[j+gap] = arr[j];
} else {
break;
};
};
arr[j+gap] = tmp;
};
return arr;
};
/**
* @desc 希尔排序
* @sort 升序排序
* @param {Array} arr
* @return {Array}
*/
function shell( arr ) {
// 检验:非数组、空数组直接结束函数调用。
if (Object.prototype.toString.call(arr) !== '[object Array]' || arr.length === 0) {
return [];
}
if (arr.length === 1) {
return arr;
};
let len = arr.length;
let gap = 0;
while (gap < len/3) {
gap = 3 * gap + 1;
};
for (; gap > 0; gap = Math.floor((gap-1)/3)) {
console.log(gap);
arr = insert(arr, gap);
};
return arr;
}
算法分析
最佳时间复杂度:O(n)
平均时间复杂度:O(nlogn)
最坏时间复杂度:O(nlogn)
空间复杂度:O(1)
2、快速排序
算法原理
(1)从原始数组中随机取出基准,然后,把比这个数小的放在这个数左边,比这个数大的放在这个数右边,一样大的和这个数放在一起
(2)基准的左右两边各自重复上述过程,直到左边或右边只剩下一个数(或零个数)无法继续为止。
递归实现
/**
* @desc 快速排序
* @sort 升序排序
* @param {Array} arr
* @return {Array}
*/
function quick( arr ) {
// 检验:非数组、空数组直接结束函数调用。
if (Object.prototype.toString.call(arr) !== '[object Array]' || arr.length === 0) {
return [];
}
if (arr.length === 1) {
return arr;
};
let less = [];
let pivotList = [];
let more = [];
let result = [];
let length = arr.length;
let pivot = arr[0];
for (let i = 0; i < length; i++) {
if (arr[i] < pivot) {
less.push(arr[i]);
} else if (arr[i] > pivot) {
more.push(arr[i]);
} else {
pivotList.push(arr[i]);
};
};
less = quick(less);
more = quick(more);
result = result.concat(less, pivotList, more);
return result;
};
迭代实现:同递归版本的显著区别在于,使用 stack 显式维护中间状态
/**
* @desc 分片
* @param {Array} arr 待分片的数组
* @param {Number} low 分片比较的下界
* @param {Number} high 分片比较的上界
* @return {Array}
*/
function partition(arr, low, high) {
let pivot = arr[high];
let i = low - 1;
for (let j = low; j < high; j++) {
if (arr[j] <= pivot) {
i++;
[arr[i], arr[j]] = [arr[j], arr[i]];
};
};
[arr[i+1], arr[high]] = [arr[high], arr[i+1]];
return i+1;
};
/**
* @desc 快速排序
* @sort 升序排序
* @param {Array} arr
* @return {Array}
*/
function quick( arr ) {
// 检验:非数组、空数组直接结束函数调用。
if (Object.prototype.toString.call(arr) !== '[object Array]' || arr.length === 0) {
return [];
}
if (arr.length === 1) {
return arr;
};
let low = 0;
let high = arr.length - 1;
let pivot;
let stack = [low, high];
while (stack.length) {
high = stack.pop();
low = stack.pop();
pivot = partition(arr, low, high);
if (low < pivot - 1) {
stack.push(low);
stack.push(pivot - 1);
};
if (pivot + 1 < high) {
stack.push(pivot + 1);
stack.push(high);
};
};
return arr;
};
算法分析
最佳时间复杂度:O(nlogn)
平均时间复杂度:O(nlogn)
最差时间复杂度:O(n2)
空间复杂度:O(nlogn)
3、归并排序
算法原理
(1)将序列每相邻两个数字进行归并操作(merge),形成floor(n/2)个序列,排序后每个序列包含两个元素
(2)将上述序列再次归并,形成floor(n/4)个序列,每个序列包含四个元素
(3)重复步骤2,直到所有元素排序完毕
递归实现
/**
* @desc 归并
* @param {Array} left
* @param {Array} right
* @return {Array}
*/
function mergeArray( left, right ) {
let result = [];
while ( left.length > 0 && right.length > 0 ) {
if ( left[0] < right[0] ) {
result.push(left.shift());
} else {
result.push(right.shift());
};
};
return result.concat(left,right);
};
/**
* @desc 归并排序
* @sort 升序排序
* @param {Array} arr
* @return {Array}
*/
function merge( arr ) {
// 检验:非数组、空数组直接结束函数调用。
if (Object.prototype.toString.call(arr) !== '[object Array]' || arr.length === 0) {
return [];
}
let length = arr.length;
if ( length <= 1 ) {
return arr;
};
let middle = Math.floor(length/2);
let left = merge(arr.slice(0,middle));
let right = merge(arr.slice(middle,length));
return mergeArray(left, right);
};
迭代实现
/**
* @desc 归并
* @param {Array} left
* @param {Array} right
* @return {Array}
*/
function mergeArray( left, right ) {
let result = [];
while ( left.length > 0 && right.length > 0 ) {
if ( left[0] < right[0] ) {
result.push(left.shift());
} else {
result.push(right.shift());
};
};
return result.concat(left,right);
};
/**
* @desc 归并排序
* @sort 升序排序
* @param {Array} arr
* @return {Array}
*/
function merge( arr ) {
// 检验:非数组、空数组直接结束函数调用。
if (Object.prototype.toString.call(arr) !== '[object Array]' || arr.length === 0) {
return [];
}
if (arr.length === 1) {
return arr;
};
// 构造待归并的二维数组
let work = arr.map(item => [item]);
if (arr.length % 2 === 1) {
work.push([]);
};
for (let i = arr.length; i > 1; i = (i+1)/2) {
let j = 0;
for (let k = 0; k < i; j++, k+=2) {
work[j] = mergeArray(work[k], work[k+1]);
};
work[j] = [];
};
return work[0];
};
算法分析
最佳时间复杂度:O(nlogn)
平均时间复杂度:O(nlogn)
最差时间复杂度:O(nlogn)
空间复杂度:O(n)
4、堆排序
算法原理
(1)将数组构建成大顶堆,将堆顶节点取出放入有序数组中。
(2)重复步骤 1。
代码实现
/**
* @desc 堆化:将数组构建成堆的形态
* @param {Array} arr 待调整的数组
* @param {Number} i 待调整二叉树的顶部节点
* @param {Number} len 待调整的数组大小
* @return {void}
*/
function heapify(arr, i, len) {
// 先取出当前元素 i
let tmp = arr[i];
// 从 i 节点的左子节点开始,也就是 2i+1 处开始
for(let k = i * 2 + 1; k < len; k = k * 2 + 1){
// 找出左子节点、右子节点中的最大值的下标
if (k + 1 < len && arr[k] < arr[k + 1]) {
k++;
}
// 如果子节点大于父节点,将子节点值赋给父节点(不用进行交换)
if (arr[k] > tmp) {
arr[i] = arr[k];
i = k;
} else {
break;
}
}
// 将 tmp 值放到最终的位置
arr[i] = tmp;
}
/**
* @desc 堆排序
* @sort 升序排序
* @param {Array} arr 待调整的数组
* @return {Array}
*/
function heapSort(arr) {
// 检验:非数组、空数组直接结束函数调用。
if (Object.prototype.toString.call(arr) !== '[object Array]' || arr.length === 0) {
return [];
}
if (arr.length === 1) {
return arr;
};
// 构建大顶堆
for (let i = parseInt(arr.length/2 - 1); i >= 0; i--) {
// 从第一个非叶子节点从下至上,从右至左调整结构
heapify(arr, i, arr.length);
}
// 堆排序
for (let j = arr.length - 1; j > 0; j--) {
// 交换堆顶元素与末尾元素
[arr[0], arr[j]] = [arr[j], arr[0]];
// 重新对堆进行调整
heapify(arr, 0, j);
}
return arr;
}
算法分析
最佳时间复杂度:O(nlogn)
平均时间复杂度:O(nlogn)
最差时间复杂度:O(nlogn)
空间复杂度:O(1)
四、基于统计的排序算法
桶排序、计数排序、基数排序,三者都是基于统计的排序原理。所以笔者在复习排序算法时,都将这三者归为一类。
1、桶排序
算法原理
(1)将待排序数组中的数,按照一定策略分组,依次放入大小不一的桶中。
(2)然后对每个桶内的元素进行排序。
(3)输出排序结果。
代码实现
/**
* @desc 桶排序
* @sort 升序排序
* @param {Array} arr
* @return {Array}
*/
function bucket ( arr ) {
// 检验:非数组、空数组直接结束函数调用。
if (Object.prototype.toString.call(arr) !== '[object Array]' || arr.length === 0) {
return [];
}
if (arr.length === 1) {
return arr;
};
let max = Math.max.apply(Math, arr);
let min = Math.min.apply(Math, arr);
let len = arr.length
let bucket = [];
let result = [];
// 遍历原数组,将数组元素归入相应桶内
for(let k = 0; k < len; k++){
index = parseInt(( arr[k] - min ) / len );
bucket[index] = !bucket[index] ? [] : bucket[index];
bucket[index].push( arr[k] );
};
// 根据计数数组输出排序后的数组
for(let l = 0; l < bucket.length; l++){
if (!!bucket[l]) {
// 桶排序内部需要使用其他排序来给桶排列
bucket[l].sort((a, b) => a - b);
result = result.concat( bucket[l] );
};
};
return result;
};
算法分析
最佳时间复杂度:O(n)
平均时间复杂度:O(n+k)
最差时间复杂度:O(n2)
空间复杂度:O(n+k)
2、计数排序
算法原理
(1)将待排序数组中的数,生成计数分布数组。
(2)利用数组的有序性,从头遍历数组,输出排序结果。
代码实现
/**
* @desc 计数排序
* @sort 升序排序
* @param {Array} arr
* @return {Array}
*/
function count ( arr ) {
// 检验:非数组、空数组直接结束函数调用。
if (Object.prototype.toString.call(arr) !== '[object Array]' || arr.length === 0) {
return [];
}
if (arr.length === 1) {
return arr;
};
// 计数排序使用数组建立频率分布表,对负数进行排序,需要特殊处理一下。
let min = Math.min.apply(Math, arr);
if (min < 0) {
arr = arr.map(item => item + 1 - min);
};
let max = Math.max.apply(Math, arr);
let count = [];
let result = [];
// 建立计数数组
for(let j = 0; j < max + 1; j++){
count.push(0);
};
// 遍历原数组,形成原数组元素的频数分布
arr.forEach((item) => count[item] += 1);
// 根据计数数组输出排序后的数组
for (let l = 0; l < count.length; l++) {
for (let m = 0; m < count[l]; m++) {
result.push(l);
};
};
if (min < 0) {
result = result.map(item => item + min - 1);
};
return result;
};
算法分析
待排序的数据范围很大(0 ~ k),计数排序需要大量内存,是典型的以空间换时间的排序算法。最佳时间复杂度:O(n+k)
平均时间复杂度:O(n+k)
最差时间复杂度:O(n+k)
空间复杂度:O(n+k)
3、基数排序
算法原理
(1)将待排序数组中的数,根据二进制基数,从高位或低位,迭代生成桶大小不一致的桶。
(2)然后从小到大遍历各个桶,输出排序结果。
代码实现
/**
* @desc 基数排序
* @sort 升序排序
* @param {Array} arr
* @return {Array}
*/
function base( arr ) {
// 检验:非数组、空数组直接结束函数调用。
if (Object.prototype.toString.call(arr) !== '[object Array]' || arr.length === 0) {
return [];
}
if (arr.length === 1) {
return arr;
};
// 基数排序使用数组建立频率分布表,对零和负数进行排序,需要特殊处理一下。
let min = Math.min.apply(Math, arr);
if (min <= 0) {
arr = arr.map(item => item + 1 - min);
};
let maxDigit = Math.max.apply(Math, arr).toString().length;
let quenes = [];
let mod = 10;
let dev = 1;
for(let b = 0; b < maxDigit; b++, dev *= 10, mod *= 10){
// 将对应元素分配到对应的桶内
for(let i = 0, len = arr.length; i < len; i++){
let bucket = parseInt( (arr[i] % mod) / dev );
quenes[bucket] = !quenes[bucket] ? [] : quenes[bucket];
quenes[bucket].push( arr[i] );
};
// 输出本趟排序结果
let index = 0;
for(let j = 0; j < quenes.length; j++){
if (!!quenes[j]) {
for(let k = 0, len = quenes[j].length; k < len; k++){
arr[index++] = quenes[j].shift();
};
};
};
};
if (min <= 0) {
arr = arr.map(item => item + min - 1);
};
return arr;
};
算法分析
k 为数组中的最大数的二进制位数。最佳时间复杂度:O(n * k)
平均时间复杂度:O(n * k)
最差时间复杂度:O(n * k)
空间复杂度:O(n+k)
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。