回顾选择排序,插入排序,冒泡排序,快速排序,希尔排序,归并排序,堆排序以及如何计算时间复杂度
学习文章:hahda同学的javascript描述数据结构、hustcc等同学的十大经典算法
本文代码也上传到了 排序算法回顾(javascript)。
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(nlogn)
算法稳定性:不稳定
思路:先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行依次直接插入排序。
function shellSort(arr) {
var len = arr.length,
temp,
gap = 1;
while (gap < len/3) {
gap = gap*3 + 1;
}
for (gap; gap > 0; gap = Math.floor(gap/3)) {
for (var i = gap; i < len; i++) {
temp = arr[i];
for (var j = i-gap; i >= 0 && arr[j] > temp; j-=gap) {
arr[j + gap] = arr[j];
}
arr[j + gap] = temp;
}
}
return arr;
}
let arr = [10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
console.log(shellSort(arr)); // [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ]
6.归并排序
归并排序采用分治法的思想。这里给出一种自上而下递归的方法。
思路:分半->分半->再分半->分到每组只剩下一个元素的时候就回溯
平均时间复杂度:O(nlogn)
算法稳定性: 稳定
function mergeSort(arr) {
var len = arr.length;
if (len < 2) {
return arr;
}
var middle = Math.floor(len/2),
left = arr.slice(0, middle),
right = arr.slice(middle);
return merge(mergeSort(left), mergeSort(right))
}
function merge(left, right) {
var result = [];
while (left.length && right.length) {
if (left[0] <= right[0]) {
result.push(left.shift());
} else {
result.push(right.shift());
}
}
while (left.length) result.push(left.shift());
while (right.length) result.push(right.shift());
return result;
}
let arr = [5, 3, 6, 8, 2, 0, 1];
console.log(mergeSort(arr)); // [ 0, 1, 2, 3, 5, 6, 8 ]
7.堆排序
堆排序是指利用堆这种数据结构所设计的一种排序算法。
大项堆:每个节点的值都大于或等于其子节点的值,用于升序排序
小项堆:每个节点的值都小于或等于其子节点,用于降序排序
平均时间复杂度:O(nlogn)
算法稳定性: 不稳定
var len;
function buildMaxHeap(arr) { // 建立大顶堆
len = arr.length;
for (var i = Math.floor(len/2) - 1; i >= 0; i--) {
heapify(arr, i);
}
}
function heapify(arr, i) { // 堆调整
var left = 2 * i + 1,
right = 2 * i + 2,
largest = i;
if (left < len && arr[left] > arr[largest]) {
largest = left;
}
if (right < len && arr[right] > arr[largest]) {
largest = right;
}
if (largest != i) {
swap(arr, i, largest);
heapify(arr, largest);
}
}
function swap(arr, i, j) {
var temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
function heapSort(arr) {
buildMaxHeap(arr);
for (var i = arr.length-1; i > 0; i--) {
swap(arr, 0, i);
len--;
heapify(arr, 0);
}
return arr;
}
let arr = [4,3,8,10,11,13,7,30,17,26];
console.log(heapSort(arr)) // [ 3, 4, 7, 8, 10, 11, 13, 17, 26, 30 ]
8.如何估算时间复杂度
了解几个概念:
- O 符号表示一个运行时间的上界。
- Ω 符号表示一个运行时间的下界。
- θ 符号表示一个精准描述。
可以这样帮助理解,O 类似于 <= ,Ω 类似于 >=, θ 类似于 = ,但只能说是类似于。
(1) 计算迭代次数
如
let i = 0;
for (let i = 0; i < n; i++) {
i ++;
}
可以看到迭代次数为n,所以时间复杂度为 θ(n)
(2) 计算基本运算的频度
什么是基本运算呢?
- 在分析搜索和排序算法时,如果比较是元运算(不能再细化的运算),可以选择它为基本运算
- 矩阵乘法算法中,可以选择数量乘法运算
- 遍历链表时,可以选择设置或更新指针的运算
- 再图的遍历中可以选择访问结点的动作和被访问结点的计算
如上一份代码
let i = 0;
for (let i = 0; i < n; i++) {
i ++;
}
选择自加运算,同理得时间复杂度 θ(n)
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。