常见的排序算法

常见排序算法

前言

概念

维基百科上定义算法:一系列有限的、清晰定义的、可实现的计算机指令,并用于处理一类问题或进行计算。

算法的五大特点

  1. 输入:有零个或多个输入。
  2. 输出: 有一个或等多个输出。
  3. 有穷性: 算法在有限的步骤之后会自动结束而不会无限循环,并且每一个步骤可以在可接受的时间内完成。
  4. 确定性:每一步都有明确的意义,不存在歧义。
  5. 可行性:每一个步骤都可以分解成基本的步骤,在有限时间内实现。

一、排序算法分类

排序算法分成两大类:

  • 比较类排序:通过比较来决定元素间的相对次序,由于其时间复杂度不能突破O(nlogn),因此也称为非线性时间比较类排序。
  • 非比较类排序:不通过比较来决定元素间的相对次序,它可以突破基于比较排序的时间下界,以线性时间运行,因此也称为线性时间非比较类排序。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-F1oYFp9A-1658991216771)(./images/排序算法分类.png)]

二、时间复杂度

排序方法时间复杂度(平均)时间复杂度(最坏)时间复杂度(最好)空间复杂度稳定性
插入排序O(n^2)O(n^2)O(n)O(1)稳定
希尔排序O(n^1.3)O(n^2)O(n)O(1)稳定
选择排序O(n^2)O(n^2)O(n^2)O(1)不稳定
堆排序O(nlogn)O(nlogn)O(nlogn)O(1)不稳定
冒泡排序O(n^2)O(n^2)O(n)O(1)稳定
快速排序O(nlogn)O(n^2)O(nlogn)O(nlogn)不稳定
归并排序O(nlogn)O(nlogn)O(nlogn)O(n)稳定
计数排序O(n+k)O(n+k)O(n+k)O(n+k)稳定
桶排序O(n+k)O(n^2)O(n)O(n+k)稳定
基数排序O(n*k)O(n*k)O(n*k)O(n+k)稳定

三、常见排序算法代码实现

1、插入排序

插入排序(Insertion-Sort)的算法描述是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。

img

function insertSort(arr) {
    let len = arr.length
    let prevIndex, current
    for(let i = 1; i < len; i++) {
        prevIndex = i - 1
        current = arr[i]
        while(prevIndex >=0 && arr[prevIndex] > current) {
            arr[prevIndex + 1] = arr[prevIndex]
            prevIndex--
        }
        arr[prevIndex + 1] = current
    }
    return arr
}

分析:假设数组中有n个数字要排序,先从下标为1的位置开始往前比较,把当前的元素插到合适的位置大致需要比较一次,以此类推…最后到n-1的位置需要比较n-1次。比较替换的次数首尾相加求和:
∑ i = 1 n − 1 i = n ∗ n 2 \sum_{i=1}^{n -1}i = \frac{n * n} {2} i=1n1i=2nn
一般时间复杂度的计算忽略系数,所以插入排序的时间复杂度记为O(n^2);最坏的情况是每次都要比较到下标为零的元素,那么插入排序的比较次数就是n^2 /2;最好的情况是循环一遍,只比较一次,这样比较的次数是n-1

2、希尔排序

1959年Shell发明,第一个突破O(n2)的排序算法,是简单插入排序的改进版。它与插入排序的不同之处在于,它会优先比较距离较远的元素。希尔排序又叫缩小增量排序

img

let arr = [3,14,2,35,22,46,11,55,6,100,77,46,23,66]
function shellSort(arr) {
    if (arr == null || arr.length < 2) return arr;
    let n = arr.length;
    // 对每组间隔为 h的分组进行排序,刚开始 h = n / 2;
    for (let h = Math.floor(n / 2); h > 0; h = Math.floor(h / 2)) {
        //对各个局部分组进行插入排序
        insertSort(arr,h)
    }
    return arr;
}

function insertSort(arr, h) {
    let len = arr.length
    let prevIndex, current
    for(let i = 1; i < len; i++) {
        prevIndex = i - h
        current = arr[i]
        while(prevIndex >=0 && arr[prevIndex] > current) {
            arr[prevIndex + h] = arr[prevIndex]
            prevIndex-=h
        }
        arr[prevIndex + h] = current
    }
    return arr
}

shellSort(arr)

分析: 希尔排序时间复杂度相对比较复杂点

3、选择排序

选择排序(Selection-sort)是一种简单直观的排序算法。它的工作原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

img

function selectionSort(arr) {
	let len = arr.length
	let minIndex, temp
	for (let i = 0; i < len; i++) {
		minIndex = i
		for (let j = i + 1; j < len; j++) {
			if (arr[j] < arr[minIndex]) {
				minIndex = j
			}
		}
		temp = arr[i]
		arr[i] = arr[minIndex]
		arr[minIndex] = temp
	}
	return arr
}

分析:每一轮比较都保存剩余元素中最小的下标,一轮结束后只进行一次交换,省却了每次比较完之后的元素交换
∑ i = 1 n − 1 i = n ∗ n 2 \sum_{i=1}^{n -1}i = \frac{n * n} {2} i=1n1i=2nn

4、冒泡排序

冒泡排序是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。

img

function bubbleSort(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] > arr[j+1]) {       // 相邻元素两两对比
                let temp = arr[j+1];       // 元素交换
                arr[j+1] = arr[j];
                arr[j] = temp;
            }
        }
    }
    return arr;
}

分析:冒泡排序和插入排序类似,就不做解释。

5、快速排序

快速排序的基本思想:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。

img

function quickSort(arr, left, right) {
    let len = arr.length,
        partitionIndex,
        left =typeof left !='number'? 0 : left,
        right =typeof right !='number'? len - 1 : right;
 
    if(left < right) {
        partitionIndex = partition(arr, left, right);
        quickSort(arr, left, partitionIndex-1);
        quickSort(arr, partitionIndex+1, right);
    }
    return arr;
}
 
function partition(arr, left ,right) {    // 分区操作
    let pivot = left,                     // 设定基准值(pivot)
        index = pivot + 1;
    for(let i = index; 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) {
    let temp = arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
}

分析:以一个元素为基准,比完一轮,基准更换下一个元素,直到n-1个元素,比较大小的次数(n-1,n-2,n-3…1)

∑ i = 1 n − 1 i = n ∗ n 2 \sum_{i=1}^{n -1}i = \frac{n * n} {2} i=1n1i=2nn

6、计数排序

计数排序不是基于比较的排序算法,其核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。 作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。

img

function countingSort(arr, maxValue) {
    let bucket =new Array(maxValue + 1),
        sortedIndex = 0,
        arrLen = arr.length,
        bucketLen = maxValue + 1;
 
    // 计算元素个数
    for(let i = 0; i < arrLen; i++) {
        if(!bucket[arr[i]]) {
            bucket[arr[i]] = 0;
        }
        bucket[arr[i]]++;
    }
 
    for(let j = 0; j < bucketLen; j++) {
        while(bucket[j] > 0) {
            arr[sortedIndex++] = j;
            bucket[j]--;
        }
    }
 
    return arr;
}

分析:计数排序是一个稳定的排序算法。当输入的元素是 n 个 0到 k 之间的整数时,时间复杂度是O(n+k),空间复杂度也是O(n+k),其排序速度快于任何比较排序算法。当k不是很大并且序列比较集中时,计数排序是一个很有效的排序算法。

7、基数排序

基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。

img

let counter = [];
function radixSort(arr, maxDigit) {
    let mod = 10;
    let dev = 1;
    for(let i = 0; i < maxDigit; i++, dev *= 10, mod *= 10) {
        for(let j = 0; j < arr.length; j++) {
            let bucket = parseInt((arr[j] % mod) / dev);
            if(counter[bucket]==null) {
                counter[bucket] = [];
            }
            counter[bucket].push(arr[j]);
        }
        let pos = 0;
        for(let j = 0; j < counter.length; j++) {
            let value =null;
            if(counter[j]!=null) {
                while((value = counter[j].shift()) !=null) {
                      arr[pos++] = value;
                }
          }
        }
    }
    return arr;
}

分析:假设最大数的位数有k位,每次分类n次,那么时间复杂度是O(n*k)

参考博客:

​ https://www.cnblogs.com/onepixel/articles/7674659.html

​ https://www.cnblogs.com/chengxiao/p/6104371.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值