常见的十种排序,你知道几种

90 阅读13分钟

最近在学习过程中,我遇到了一些排序相关的问题。这让我想起了之前面试的过程中曾经问过的一个问题:”你知道几种排序“。为了加深自己对排序算法的理解,我决定重新整理这个问题并分享我的答案。

生成数据

在排序之前我们需要先写一个生成乱序数据的方法

可以分成两步:

1、生成一个有序数组

2、将起随机打乱

如何打乱呢?

我们可以新建一个数组,每次从数组中随机取出一个并按抽取照顺序依次放入数组中。

为了避免内存的浪费,可以在原数组上做上述类似的操作,将数组分为两部分,末端为已打乱,其他部分为未打乱,

例如:

step1:从所有数组中随机选取一个,与最后一个元素交换位置,这样最后一个元素为已打乱,假设数组长度为length,前面length-1 个元素为待打乱,

step2:从lenght-1 中随机抽取一个元素,与倒数第二个元素交换,这样就有两个元素是已打乱的,以此类推。

我们将上述逻辑转化为代码:

// 交换元素的方法,后续就不重复写了
function swap(arr, i, j) {
    if (i === j) {
        return;
    }
    let temp = arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
}

function randomArr(len) {
    const arr = [];
    for (let i = 0; i < len; i++) {
        let randomIndex = Math.floor(Math.random() * (len - i)) + i;
        let changeIndex = len - i - 1;
        if (typeof arr[changeIndex] === 'undefined') {
            arr[changeIndex] = changeIndex;
        }
        if (typeof arr[randomIndex] === 'undefined') {
            arr[randomIndex] = randomIndex;
        }

        swap(arr, randomIndex, changeIndex);
    }
    return arr;
}

上述代码中我们将生成有序数组和打乱过程合并在一个for循环中了,如果在原始数组上读取不到值,那么我们就先赋值再做交换处理。

排序算法

有了上述生成数据的方法,我们可以编写我们的排序算法啦,首先想到的是三种非常经典的排序。

插入排序

插入排序的思想很简单,从第二个元素开始,向前寻找合适的位置,然后插入其中,在寻找合适位置的过程中同样需要遍历,用一张图表示

https://upload.wikimedia.org/wikipedia/commons/0/0f/Insertion-sort-example-300px.gif

function insertSort(arr) {
    let len = arr.length;
    for (let i = 1; i < len; i++) {
        // 寻找合适的位置
        let temp = arr[i];
        let insertIndex = i - 1;
        while (insertIndex >= 0 && arr[insertIndex] > temp) {
            arr[insertIndex + 1] = arr[insertIndex];
            insertIndex--;
        }
        arr[insertIndex + 1] = temp;
    }
    return arr;
}

冒泡排序

从第一个元素开始,依次和后面一个元素作比较,如果后面的元素较大,则互换位置,当最后一个元素比较完成时最大的元素一定会置换到最后的位置去,我们对剩下的元素做同样的操作就可以依次固定最大的,次大的元素。整个过程看上就就是把最大的元素不断地向后冒泡。

https://upload.wikimedia.org/wikipedia/commons/c/c8/Bubble-sort-example-300px.gif

function bubbleSort(arr){
	for (let i = 0; i < arr.length; i++) {
	    for (let j = 0; j < arr.length - i - 1; j++) {
	        if (arr[j] > arr[j + 1]) {
	            let temp = arr[j];
	            arr[j] = arr[j + 1];
	            arr[j + 1] = temp;
	        }
	    }
	}
	return arr;
}

选择排序

找到最小的元素记住它,与第一个元素交换位置,然后找剩下的元素中最小的,与第二个元素交换位置,以此类推

849589-20171015224719590-1433219824 (1).gif

849589-20171015224719590-1433219824 (1).gif

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

以上排序几乎没有开辟新的空间来存储子数组,所以空间复杂度是O(1), 但是都是两次嵌套循环,所以时间复杂度是O(n^2)

下面我们来看一些牺牲空间换时间的排序算法。

计数排序

上述我们生成的数据是从0开始的,并且是不重复的,最后排序的结果一定是[0,1,2,3…],key和value 是相等的

如果对于这种数组来说,如果我们新开辟一个数组,直接讲值最为key放入新的数组中,则新数组就是排序的结果

举个例子:

原始数据 [6,5,4,3,2,1,0]

生成一个新数组[]

  • 第一个元素是6, 放入新数组的索引为6的位置[,6]
  • 第二个元素是5 放入新数组的索引为5的位置[,5,6]
  • … 最后变成了 [0,1,2,3,4,5,6]

只需要遍历一遍,数据的排序就好了,其核心思想是,value 作为新数组的key,新数组中自然就会将其排列好。

如果将上述思想再扩展一下,就是所谓的计数排序,

我们需要考虑到数据可能是重复的,数据起始位置不一定从0开始,我们可以将数据作为新的数组的key,但是考虑到重复的数据,我们需要记录一下这个值出现的次数,我们可以将新数组的形式设计为下面这种格式

[key<原始数据的value> : value<原始数据中出现了几次>]

举个例子

原始数据为 [3,3,1,2,2,0]

3出现了2次

1出现了1次

2出现了2次

0出现了1次

那么新数组为 [0:1,1:1,2:2,3:3]

这个新数组记录的就是原始数据中值出现的次数,我们称之为计数数组,

最后按照这个计数结果依次展开不就是最后排序的结果了

[0,1,2,2,3,3]

这种排序对于上述我们演示的数据来说效率极高,但是对于数据跨度很大的数据来说会浪费很多不必要的空间。而且不太适合value 为非数字类型的数组

比如 原始数据为[0,100]

计数数组的长度会变成100,本来只有两个数据,但是我们却要建立长度为100的计数数组,很显然这会浪费额外的空间

849589-20171015231740840-6968181.gif

// 获取最大值和最小值
function getCountInfo(arr){
	let min = arr[0];
	let max = arr[0];
	let canCount = true;
	for(let i=0;i<arr.length;i++){
		if(typeof arr[i]!=='number'){
			canCount = false;
			break;
		}
		if(arr[i]>max){
			max = arr[i]
		}
		else if(arr[i]<min){
			min = arr[i]
		}
	}
	return {min,max,canCount};
}

function countingSort(arr){
	const {min,max,canCount} = getCountInfo(arr);
	if(!canCount){
		return arr;
	}
	const countingArr = [];
	for(let i=0;i<arr.length;i++){
		let countIndex = arr[i]-min;
		if(typeof countingArr[countIndex] === 'undefined'){
			countingArr[countIndex] = 0;
		}
		countingArr[countIndex]++;
	}
	let originIndex = 0;
	for(let i=0;i<countingArr.length;i++){
		let amount = countingArr[i];
		for(let j=0;j<amount;j++){
			let originValue = i+min;
			arr[originIndex++] = originValue;
		}
	}
	return arr;
}

接下来几种排序都有点”分治“的思想,

分治法(英语:Divide and conquer)字面上的解释是“分而治之”,就是把一个复杂的问题分成两个或更多的相同或相似的子问题,直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。

举个例子对于一个大的数组,第一步将数据分成两部分,小的数据和大的数据,然后再讲小的数据继续划分,直到划分的结果只剩下两个数字,最后问题就解决了。

接下来我们来看看具体是怎么分治的。

快速排序

快速排序的思想在于

1、先找一个基准数字,比它小的放在左边,比它大的放在右

2、这样就把数据分成了两部分,

3、用同样的方法把上述小的数组继续细分,直到小数组中只剩下两个数字

这就是上述提到的分治的思想,把大问题拆成小问题,只要这个拆分的尽头问题解决了那么整个问题都解决了。

那编码的关键在于如何将一个数组按照某个基准数分成两部分,基准数我们可以采用数组的第一个元素,

方法一:从基准元素后面一个元素开始遍历,比这个基准元素小的都依次跟在基准元素后面

举个例子:原始数据为 3,6,4,5,2,1

我们以3 为基准,从6开始遍历,如果遇到小的就跟在3后面

  • 6>3 所以先不动
  • 4>3 所以先不动
  • 5>3 先不动
  • 2<3 ,让2排在3后面,这时候6就需要腾位置了,所以我们将2和6交换
  • 数组变成 3,2,4,6,1
  • 1<3 ,我们需要跟在3后面,2已经占据了第一个位置,那么1只能去3后面的第二个位置(4的位置)的位置
  • 所以1与4进行交换,
  • 最终变成 3,2,1, 4,6
  • 数据分成了两部分,321 和 46 ,基准数据后面跟着的都是比它小的,所以我们将基准数据放到前半部分的最后,3和1交换位置
  • 数据变成1,2 3 ,4,6
  • 这样用基准数据分成大数据和小数据就完成啦

849589-20171015230936371-1413523412.gif 我们将上述过程变成代码

function quickSort(arr,start=0,end=arr.length){
	if(start>=end){
		return;
	}
	let splitIndex = partion(arr,start,end);
	quickSort(arr,start,splitIndex-1);
	quickSort(arr,splitIndex,end);
}

function partion(arr,start=0;end=arr.length){
	let baseValue = arr[start];
	let minOffset = start;
	for(let i = start+1;i<end;i++){
		if(arr[i]<baseValue){
			swap(arr,i,minOffset);
			minOffset++;
		}
	}
	swap(arr,minOffset,start);
	return minOffset;
}

方法二:我们也可以使用双指针来操作

这篇文章对于双指针讲解的比较详细 快速排序算法详解(原理、实现和时间复杂度) (biancheng.net)

// 双指针分发
function partion(arr, left = 0, right = arr.length) {
    let startIndex = left;
    let endIndex = right - 1;
    while (startIndex < endIndex) {
        while (arr[startIndex] <= arr[endIndex] && startIndex < endIndex) {
            endIndex--;
        }
        swap(arr, startIndex, endIndex);
        while (arr[startIndex] <= arr[endIndex] && startIndex < endIndex) {
            startIndex++;
        }
        swap(arr, startIndex, endIndex);
    }
    return startIndex;
}

归并排序

归并排序是一种分治算法,它将一个大数组分成两个或更多个小数组,对每个子数组进行排序,然后将排序后的子数组合并成一个已排序的大数组。以下是归并排序的基本步骤:

  1. 将数组分成两个(或更多)子数组。如果数组只有一个元素,则已经排序,不需要进一步操作。
  2. 对每个子数组进行归并排序(递归地应用归并排序算法)。
  3. 将排序后的子数组合并成一个已排序的大数组。

合并过程如下:

  1. 创建两个指针,一个指向第一个子数组的第一个元素,另一个指向第二个子数组的第一个元素。
  2. 比较两个指针所指向的元素。将较小(或较大,取决于排序顺序)的元素添加到结果数组中,并将相应的指针向前移动一位。
  3. 重复步骤2,直到一个子数组中的所有元素都被添加到结果数组中。
  4. 将另一个子数组中剩余的元素添加到结果数组中。

mergeSort.gif

function mergeSort(arr) {
        if (arr.length < 2) {
            return arr;
        }
        let middle = Math.floor(arr.length / 2);
        let leftArr = arr.slice(0, middle);
        let rightArr = arr.slice(middle, arr.length);
        arr = null;
        return merge(mergeSort(leftArr), mergeSort(rightArr));
    }
    function merge(left = [], right = []) {
        const res = [];
        let leftIndex = 0,
            rightIndex = 0;
        while (leftIndex < left.length || rightIndex < right.length) {
            let leftValue = left[leftIndex] ?? Infinity;
            let rightValue = right[rightIndex] ?? Infinity;
            if (leftValue < rightValue) {
                res.push(leftValue);
                leftIndex++;
            } else {
                res.push(rightValue);
                rightIndex++;
            }
        }
        left = null;
        right = null;
        return res;
    }

堆排序

堆排序是一种基于比较的排序算法,它利用数据结构二叉堆(通常为最大堆或最小堆)对数组进行排序。以下是堆排序的基本步骤:

  1. 将数组构建成一个最大堆(或最小堆,取决于排序顺序)。
  2. 将堆顶元素(最大值或最小值)与堆的最后一个元素交换。
  3. 移除堆中的最后一个元素,将其添加到已排序部分。
  4. 对剩余的堆进行调整,使其重新满足最大堆(或最小堆)的性质。
  5. 重复步骤2-4,直到堆为空。

构建最大堆的过程如下:

  1. 从数组的一半处开始向前遍历,对每个元素执行下沉操作。
  2. 下沉操作:将当前元素与其子节点中的较大者(或较小者,取决于排序顺序)交换,直到它满足堆的性质(即当前元素大于(或小于)其子节点)。

1258817-20190420150936225-1441021270.gif

function heapSort(arr) {
        buildMaxHeap(arr);
        let len = arr.length;
        for (let i = arr.length - 1; i >= 0; i--) {
            swap(arr, 0, i);
            len--;
            heapify(arr, 0, len);
        }
        return arr;
    }
    function buildMaxHeap(arr) {
        let len = arr.length;
        for (let i = Math.floor(len / 2); i >= 0; i--) {
            heapify(arr, i, len);
        }
    }
    function heapify(arr, i, len = arr.length) {
        let left = 2 * i + 1;
        let right = 2 * i + 2;
        let 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, len);
        }
    }

桶排序

桶排序(Bucket sort)是一种线性排序算法,它的基本思想是将待排序数据分到有限数量的桶子里,然后对每个桶子里的数据进行排序,最后将所有桶子里的数据依次取出,即可得到排好序的序列。桶排序的时间复杂度为O(n+k),其中n是待排序元素的个数,k是桶的数量。

以下是桶排序的过程:

  1. 确定桶的数量和每个桶能够容纳的数据范围。
  2. 遍历待排序序列,将每个元素放到对应的桶中。
  3. 对每个桶中的元素进行排序。
  4. 按照桶的顺序,依次将所有元素取出,即可得到排好序的序列。

需要注意的是,如果待排序元素的分布比较均匀,则每个桶中的元素数量不会太多,排序效率较高。但如果待排序元素分布不均匀,则某些桶中的元素数量可能会很多,需要考虑如何优化桶内排序算法。

function bucketSort(arr, bucketSize = 5, min = 0, max = arr.length - 1) {
        let buckets = [];
        function getBucketIndex(value) {
            let index = Math.floor((value - min) / bucketSize);
            return index;
        }
        function insertToArr(bucketArr, value) {
            if (!bucketArr) {
                return [value];
            }
            let insertIndex = 0;
            while (
                bucketArr[insertIndex] < value &&
                insertIndex < bucketArr.length
            ) {
                insertIndex++;
            }

            for (let i = bucketArr.length; i > insertIndex; i--) {
                bucketArr[i] = bucketArr[i - 1];
            }
            bucketArr[insertIndex] = value;
            return bucketArr;
        }
        for (let i = 0; i < arr.length; i++) {
            let bucketIndex = getBucketIndex(arr[i]);
            if (!buckets[bucketIndex]) {
                buckets[bucketIndex] = [];
            }
            insertToArr(buckets[bucketIndex], arr[i]);
        }
        let index = 0;
        for (let i = 0; i < buckets.length; i++) {
            for (let j = 0; j < buckets[i].length; j++) {
                arr[index++] = buckets[i][j];
            }
        }
        return arr;
    }

基数排序

基数排序(Radix sort)是一种非比较型整数排序算法,它的基本思想是将待排序元素按照位数依次排序,从低位到高位,直到所有位数排序完成。基数排序可以采用LSD(Least significant digit)和MSD(Most significant digit)两种方式实现。其中,LSD方式从低位到高位依次排序,MSD方式从高位到低位依次排序。

以下是LSD方式实现基数排序的过程:

  1. 将待排序元素按照个位数的大小放入桶中。
  2. 按照桶的顺序,依次将所有元素取出,组成新的序列。
  3. 将新的序列按照十位数的大小放入桶中。
  4. 按照桶的顺序,依次将所有元素取出,组成新的序列。
  5. 重复步骤3和4,直到所有位数都排好序。

基数排序的时间复杂度为O(d(n+k)),其中d是元素的最大位数,k是每个桶中元素的数量。基数排序适用于元素数量大、位数少的序列排序。

https://images2017.cnblogs.com/blog/849589/201710/849589-20171015232453668-1397662527.gif

function radixSort(arr) {
        let countingArr = new Array(10);
        let len = arr.length;
        let position = 1;
        function getNumByPosition(num, position) {
            const res = parseInt((num % (position * 10)) / position);
            return isNaN(res) ? 0 : res;
        }
        while (len / position > 1) {
            countingArr.fill(null);

            for (let i = 0; i < len; i++) {
                let positionNum = getNumByPosition(arr[i], position);
                if (!countingArr[positionNum]) {
                    countingArr[positionNum] = [];
                }
                countingArr[positionNum].push(arr[i]);
            }
            // 调整顺序
            let temp = [];
            let index = 0;
            for (let j = 0; j < countingArr.length; j++) {
                for (let k = 0; countingArr && k < countingArr[j].length; k++) {
                    temp[index] = countingArr[j][k];
                    index++;
                }
            }
            arr = temp;
            position = position *= 10;
        }
        return arr;
    }

希尔排序

https://images2018.cnblogs.com/blog/849589/201803/849589-20180331170017421-364506073.gif

function shellSort(arr) {
        for (
            let gap = Math.floor(arr.length / 2);
            gap >= 1;
            gap = Math.floor(gap / 2)
        ) {
            for (let i = gap; i < arr.length; i++) {
                for (let j = i; j >= gap && arr[j] < arr[j - gap]; j -= gap) {
                    swap(arr, j, j - gap);
                }
            }
        }
        return arr;
    }