该内容是学习B站左神的算法课生产的。
时间复杂度 O (读big O)
常数操作的时间 —— 一个操作如果和样本的数据量没有关系,每次都是固定时间完成的操作,叫做常数操作。
时间复杂度为一个算法流程中,常熟操作数量的一个指标。用O
(读big O)来表示,首先要对算法流程非常熟悉,写出算法中发生了多少常数操作,然后总结成常数操作数量的表达式,表达式中,不要低阶项、常数,也不要高阶项的系数,只要高阶项,剩下部分如果是 f(n)
,那么时间复杂度为O(f(n))
评价一个算法流程的好坏,先看时间复杂度的指标,再分析不同数据样本的实际运行时间,也就是“常数项时间”。
1. 选择排序
遍历未排序的数据寻找最小/大的数,放入已排序的末尾
时间复杂度:O(n^2)
与数据无关
空间复杂度:O(1)
function select_sort(arr) {
if (arr.length < 2 || arr == null) return;
for (let i = 0; i < arr.length; i++) {
let minIndex = i; // 假设当前i为最小index
for (let j = i + 1; j < arr.length; j++) {
if (arr[minIndex] > arr[j]) { //查询后边的值发现比假设值更小
swap(arr, minIndex, j);// 交换
}
}
}
return arr;
}
function swap(arr, a, b) {
arr[a] = arr[a] ^ arr[b];
arr[b] = arr[a] ^ arr[b];
arr[a] = arr[a] ^ arr[b];
}
2. 冒泡排序
遍历数据,两两对比,将小/大的放前面,比较n轮
时间复杂度:O(n^2)
与数据无关
空间复杂度:O(1)
function pop_sort(arr) {
if (arr == null || arr.length < 2) return;
const len = arr.length;
// for (let i = 0; i < len; i++) {
for (let i = len; i > 0; i--) {
for (let j = 0; j < i; j++) {
if (arr[j] > arr[j + 1]) {
swap2(arr, j, j + 1)
}
}
}
return arr;
}
function swap2(arr, i, j) {
let temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
3. 插入排序
从数据左边开始,每个数据都与前面的数做比较,谁小/大就交换位置,直至前面无数据对比或不大/小于
时间复杂度:O(n^2)
受数据影响([1,2,3,4]
和[4,3,2,1]
操作次数不同),但是以数据最差常数操作来估计
空间复杂度:O(1)
function insert_sort(arr) {
if (arr == null || arr.length < 2) return;
const len = arr.length;
for (let i = 1; i < len; i++) {
while (arr[i] < arr[i - 1] && i >= 0) {
swap(arr, i, i - 1);
i--
}
}
return arr;
}
function swap(arr, i, j) {
arr[i] = arr[i] ^ arr[j];
arr[j] = arr[i] ^ arr[j];
arr[i] = arr[i] ^ arr[j];
}
4. 归并排序
利用递归,左边排好序,右边排好序,再让整体有序
时间复杂度:O(N*logN)
空间复杂度:O(N)
const arr1 = [1, 4, 2, 4, 6, 7, 9];
// 左边排序, 右边排序, 左右对比, 创建一个数组, 谁小,谁先放进去
function merge_sort(arr, l, r) {
if (arr.length < 2 || arr == null) return arr;
if (l == r) return;
const mid = l + ((r - l) >> 1);
merge_sort(arr, l, mid);
merge_sort(arr, mid + 1, r);
return merge(arr, l, mid, r);
}
function merge(arr, l, m, r) {
let sortArr = [];
let i = 0
let p1 = l;
let p2 = m + 1;
while (p1 <= m && p2 <= r) {
sortArr[i++] = arr[p1] <= arr[p2] ? arr[p1++] : arr[p2++];
}
while (p1 <= m) {
sortArr[i++] = arr[p1++]
}
while (p2 <= r) {
sortArr[i++] = arr[p2++]
}
for (let i = 0; i < sortArr.length; i++) {
arr[l + i] = sortArr[i]
}
return arr;
}
console.log(merge_sort(arr1, 0, arr1.length - 1))
扩展:
-
小和问题
在一个数组中,每一个数左边比当前数小的数累加起来,叫做这个数组的小和。求数组小和。
const arr2 = [1, 4, 2, 3, 8, 5];
function getSum(arr, L, R) {
if (arr.length < 2 || arr == null) return;
if (L == R) return 0;
const M = L + ((R - L) >> 1);
return getSum(arr, L, M) + getSum(arr, M + 1, R) + add_up(arr, L, M, R);
}
function add_up(arr, L, M, R) {
let help = [];// 需要先排序
let p1 = L;
let p2 = M + 1;
let res = 0;
while (p1 <= M && p2 <= R) {
if (arr[p1] < arr[p2]) {
res += arr[p1] * (R - p2 + 1);
help.push(arr[p1]);
p1++
} else {
help.push(arr[p2]);
p2++
}
// res += arr[p1] < arr[p2] ? arr[p1] * (R - p2 + 1) : 0;
// arr[p1] < arr[p2] ? help.push(arr[p1++]) : help.push(arr[p2++])
}
while (p1 <= M) {
help.push(arr[p1++])
}
while (p2 <= R) {
help.push(arr[p2++])
}
console.log(res, '---');
console.log(help,'===', arr);
// 将原数组左右两边排好序
for (let i=0;i<help.length;i++) {
arr[i + L] = help[i];
}
return res;
}
console.log(getSum(arr2, 0, arr2.length - 1));
-
逆序对问题
在一个数组中,左边的数如果比右边的数大,则这两个数构成一个逆序对,打印所有的逆序对。
const arr2 = [1, 4, 2, 3, 8, 5];
function getSum(arr, L, R) {
if (arr.length < 2 || arr == null) return;
if (L == R) return 0;
const M = L + ((R - L) >> 1);
return getSum(arr, L, M) + getSum(arr, M + 1, R) + add_up(arr, L, M, R)
}
function add_up(arr, L, M, R) {
let help = [];// 需要先排序
let p1 = L;
let p2 = M + 1;
let res = 0;
while (p1 <= M && p2 <= R) {
res += arr[p1] > arr[p2] ? 1 : 0;
arr[p1] < arr[p2] ? help.push(arr[p1++]) : help.push(arr[p2++]);
}
while (p1 <= M) {
help.push(arr[p1++])
}
while (p2 <= R) {
help.push(arr[p2++])
}
// 将原数组左右两边排好序
for (let i=0;i<help.length;i++) {
arr[i + L] = help[i];
}
return res;
}
console.log(getSum(arr2, 0, arr2.length - 1));
-
荷兰国旗问题
-
给定一个数组arr,一个数num,请把小于等于num的数放在数组的左边,大于num的数放在数组的右边。要求空间复杂度为
O(1)
,时间复杂度为O(N)
。 -
给定一个数组arr,一个数num,请把小于num的数放在数组的左边,等于num的数放在数组的中间,大于num的数放在数组的右边。要求空间复杂度为
O(1)
,时间复杂度为O(N)
。
-
const arr = [1, 2,3,4,2,2,1,5];
const target = 3;
function classification(arr, target) {
let i = 0;
let small = 0;
let big = arr.length - 1;
while (i <= big) {
if (arr[i] < target) {
swap(arr, small++, i++);
} else if (arr[i] > target) {
swap(arr, big--, i)
} else {
i++
}
}
console.log(arr);
}
function swap(arr, i, j) {
let temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
classification(arr, target)
5. 快速排序
-
1.0版本,在一个数组中,总是拿某一个范围内的最后一个数做划分,分为大于这个数的和小于这个数的,这个流程做递归。
-
2.0版本,在1.0版本中,分为大于这个数、等于这个数、小于这个数。
-
1.0和2.0版本的时间复杂度在最差情况下为O(N^2)。
-
3.0版本,在数组中随机选一个数和最后一个数做交换,以它做划分。O(NlogN)
-
额外空间复杂度O(logN)
let arr = [2, 5, 1, 8, 3, 4, 7, 1, 3];
quick_sort(arr, 0, arr.length - 1);
console.log(arr);// [1, 1, 2, 3, 3, 4, 5, 7, 8]
function quick_sort(arr, l, r) {
if (l < r) {
swap(arr, l + Math.floor(Math.random() * (r-l+1)), r); // 随机选一个数和最后一个做交换
let p = partition(arr, l, r); // 划分区域的左边界和右边界
quick_sort(arr, l, p[0] - 1); // < 区
quick_sort(arr, p[1] + 1, r); // > 区
}
}
function partition(arr, l, r) {
let less = l - 1; // < 区右边界
let more = r; // >区左边界
while(l < more) { // l表示当前数的位置; arr[r]表示划分值
if (arr[l] < arr[r]) {
swap(arr, l++, ++less);
} else if (arr[l] > arr[r]) {
swap(arr, l, --more);
} else {
l++
}
}
swap(arr, more, r);
return [less + 1, more]
}
function swap(arr, i, j) {
let temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
扩:归并排序和快速排序对比
归并排序 | 快速排序 | |
---|---|---|
实现 | 将数组分成两个子数组分别排序,并将有序的子数组归并来将整个数组排序。 | 当两个子数组都有序时整个数组也就有序了。 |
递归时机 | 递归调用发生在处理整个数组之前。 | 递归调用发生在处理整个数组之后。 |
数组处理 | 数组等分为两半 | 切分(partition)位置取决于数组内容 |
6. 堆排序
-
先让整个数组都变成大根堆结构
-
从上到下heap_insert(),时间复杂度为O(N*logN)
-
从下到上heapify(),时间复杂度为O(N)
-
-
把堆的最大值和堆末尾的值进行交换,然后减少堆的大小,继而去调整重新变成大根最,一直周而复始,时间复杂度为O(N*logN)
-
堆的大小减少为0时,就拍好序了。
-
时间复杂度O(NlogN)
-
空间复杂度O(1)
let arr = [2, 1, 5, 3, 2];
console.log(heap_sort(arr))
// 堆排序
function heap_sort(arr) {
if (arr == null || arr.length < 2) return;
// heapInsert 把数组变成大根堆
// 方法一: 利用heap_insert, 从上上找
//for (let i = 0; i < arr.length; i++) {
//heap_insert(arr, i); // O(logN)
//}
// 方法二: 利用heapify, 从下到上, 不断向下将最大值找到, 实际使用效率更高
for(let i = arr.length; i >= 0; i--) {
heapify(arr, i, arr.length)
}
let heapSize = arr.length;
// 把第一个数和最后一个调换位置, heapSize - 1, 调用heapify()
swap(arr, 0, --heapSize)
while (heapSize > 0) {
heapify(arr, 0, heapSize)
swap(arr, 0, --heapSize)
}
return arr;
}
function heapify(arr, index, heapSize) {
// 左孩子
let left = index * 2 + 1;
while (left < heapSize) { // 判断是否有孩子,左孩子是否越界
// 两个孩子谁大, 谁把下标给maxVal 得先判断右孩子是否存在 得注意,当条件不成立时, 下标应该为左孩子
let maxVal = left + 1 < heapSize && arr[left + 1] > arr[left] ? left + 1 : left;
// 父亲和较大孩子的值谁大, 谁把下标给maxVal
maxVal = arr[index] > arr[maxVal] ? index : maxVal;
// 父值等于最大值, 直接break
if (maxVal == index) break;
swap(arr, index, maxVal);
index = maxVal;
left = index * 2 + 1;
}
}
function heap_insert(arr, index) {
let fa = Math.trunc((index - 1) / 2);
while (arr[index] > arr[fa]) {
swap(arr, index, fa);
index = fa;
fa = Math.trunc((index - 1) / 2);
}
// while (arr[index] > arr[Math.trunc((index - 1) / 2)]) {
// swap(arr, index, Math.trunc((index - 1) / 2))
// index = Math.trunc((index - 1) / 2)
// }
}
function swap(arr, i, j) {
let temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
未完......