回顾选择排序,插入排序,冒泡排序,快速排序,以及如何计算时间复杂度
1.选择排序
思路:从未排序的序列中选出最小(大)的元素,放进已排好序的序列末尾。
时间复杂度:O(n^2)
算法稳定性:不稳定
// 定义一个函数用于交换
function swap (array, i, j) {
let temp = array[i];
array[i] = array[j];
array[j] = temp;
}
function selectionSort (arr) {
let minIndex;
for (let i = 0; i < arr.length; i++) {
minIndex = i;
for (let j = i + 1; j < arr.length; j++) { // 对未排序的序列进行循环,找出最小元素。
if (arr[j] < arr[minIndex]) {
minIndex = j;
}
}
swap(arr, i, minIndex); // 最小元素与放如排好序的序列末尾。
}
return arr;
}
let arr = [1,2,8,4,3,6,10];
selectionSort(arr) // 1,2,3,4,6,8,10
选择排序所需要的元素比较次数为 (n-1) + (n-2) + ... + 1 = n*(n-1)/2 ,元素赋值次数界于 0 ~ 3(n-1) 之间,也就是原序列已排好序于原序列为反序两种极端情况。
2.插入排序
思路:从第二个元素往后遍历,从前面的序列中找到一个合适的位置进行插入。
时间复杂度:O(n^2)
算法稳定性:稳定
let arr = [5,3,2,6,7,10,1]; // 进行小到达排序
function InsertionSort(arr) {
let len = arr.length;
for (let i = 1; i < len; i++) {
let curr = arr[i]; // 要执行插入操作的元素
let j = i; // 从i开始往回遍历
while (j > 0 && arr[j-1] > curr) {
// 不断跟curr元素进行比较,大于curr的往后退一位,最终给curr腾出一个插入的位置
arr[j] = arr[j-1];
j--;
}
arr[j] = curr // curr插入到合适的位置中
}
return arr;
}
console.log(InsertionSort(arr)); // 1,2,3,5,6,7,10
容易看出,当序列已排好序的时候,元素比较的次数最少,比较次数为 n - 1 次,每一个元素只需要和前一个元素比较即可,当序列是按反序排列,那么比较次数最多,比较次数为 n*(n-1)/2 。
元素赋值次数为等于比较次数加上 n - 1。
3.冒泡排序
思路:多次遍历序列,比较相邻元素,将最大(最小)元素像泡泡一样冒到后面已排好序的序列中。
时间复杂度:O(n^2)
算法稳定性:稳定
function advanceBubbleSort1(arr){
let len = arr.length;
let flag; // 设置一个标记,如果某一轮没有交换,表示已经排好序了。不必再循环遍历。
for(let i = 1, i <= len - 1; i++){
flag = false;
for(let j = 0; j < len - i; j++){
if(arr[j] > arr[j + 1]){
temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
flag = true;
}
}
if(flag === false){
break;
}
}
return arr;
}
4.快速排序
快速排序是一个非常流行并且高效的排序算法。
它之所以高效是因为它在原位上进行排序,不需要辅助的存储空间。
思路:以最左元素作为主元进行划分,最后再将主元放回正确位置,递归。
平均时间复杂度 Θ(nlogn), 最坏的情况 θ(n^2)
算法稳定性:不稳定
在了解快速排序之前需要了解一个关键算法:划分算法
function partition(arr, left ,right) { // 分区操作
var pivot = left, // 设定基准值(pivot),即以最左元素为主元
index = pivot + 1;
for (var i = left + 1; i <= right; i++) {
if (arr[i] < arr[pivot]) {
swap(arr, i, index);
index++;
}
}
swap(arr, pivot, index - 1); // 最后把主元放回正确位置
return index-1;
}
function swap(arr, i, j) {
var temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
我们可以看到,整个划分都在原数组上进行,不需要引进额外的辅助数组。
快速排序算法需要以划分算法为核心:
function quickSort(arr, left, right) {
var len = arr.length,
partitionIndex;
if (left < right) {
partitionIndex = partition(arr, left, right);
quickSort(arr, left, partitionIndex-1);
quickSort(arr, partitionIndex+1, right);
}
return arr;
}
let arr = [1,6,3,8,5,0,7]
console.log(quickSort(arr, 0, 6)) // 0,1,3,5,6,7,8
5.如何估算时间复杂度
了解几个概念:
- O 符号表示一个运行时间的上界。
- Ω 符号表示一个运行时间的下界。
- θ 符号表示一个精准描述。
可以这样帮助理解,O 类似于 <= ,Ω 类似于 >=, θ 类似于 = ,但只能说是类似于。
5.1 计算迭代次数
如
let i = 0;
for (let i = 0; i < n; i++) {
i ++;
}
可以看到迭代次数为n,所以时间复杂度为 θ(n)
5.2 计算基本运算的频度
什么是基本运算呢?
- 在分析搜索和排序算法时,如果比较是元运算(不能再细化的运算),可以选择它为基本运算
- 矩阵乘法算法中,可以选择数量乘法运算
- 遍历链表时,可以选择设置或更新指针的运算
- 再图的遍历中可以选择访问结点的动作和被访问结点的计算
如上一份代码
let i = 0;
for (let i = 0; i < n; i++) {
i ++;
}
选择自加运算,同理得时间复杂度 θ(n) 本文由学什么技术好网亲情奉献