首页 【基础算法】排序算法之选择排序(直接选择排序 + 堆排序)
文章
取消

【基础算法】排序算法之选择排序(直接选择排序 + 堆排序)

下文中会用到的工具函数swap(arr, i, j)

1
2
3
4
// 交换 arr 中下标为 i 和 j 的元素值
function swap(arr, i, j) {
    [arr[i], arr[j]] = [arr[j], arr[i]];
}

测试排序算法:leetcode:912. 排序数组

狭义的“选择排序”指直接选择排序。本文默认升序。

直接选择排序

假设有n个数字,一共进行n轮,每轮遍历数组的未排序部分:第1轮选择最小的数字,放到数组第1位;第2轮选择第二小的数字,放到数组第2位……直接选相应的牌放到相应位置。

两个for循环,外循环对应一轮确定的坑位,内循环找该坑位对应的下标。

1
2
3
4
5
6
7
8
9
10
11
12
function selectionSort(arr) {
    const len = arr.length;
    for (let i = 0; i < len; i++) {
        // 第一个循环定位本轮要填的牌位
        let minIndex = i; // 搜索区间最小值下标
        for (let j = i; j < len; j++) {
            // 第二个循环找符合牌位条件的牌的下标
            if (arr[j] < arr[minIndex]) minIndex = j;
        }
        swap(arr, i, minIndex); // 交换牌位原元素和符合条件的元素
    }
}
  • 时间复杂度:o(n^2)

  • 空间复杂度:o(1)

  • 稳定性:稳定

备注:排序算法“不稳定”是指,原序列中相等的元素,假设原顺序为a1 a2 a3,排序后此顺序可能会发生改变(比如,由 a1 a2 a3 -> a2 a1 a3),即相等元素的原相对位置改变,就叫排序算法不稳定;反之,排序后相对位置一定不变,则排序算法稳定。

  • 与插入排序的区别?

选择排序是坑位视角,【“对着坑选牌”:站在坑位面前,面对一堆牌,“选择”某张牌放到面前的坑位】当前坑位为第i大的元素的位置,因此目标就是找第i大的元素;

插入排序是牌视角,【“拿着牌选坑”:拿着一张牌,面对已占好坑位的一群牌,然后伺机把当前牌插入其中】当前元素为第i大元素,因此根据元素大小一位一位往前比较坑位,挪到第i大的位置。

堆排序

堆排序的重点其实是堆的实现。如果语言内置了堆的实现,那就很简单了。

堆排序的逻辑与直接选择类似,都是对着坑选牌。首先选出最大/小的,然后选出次大/小的,以此类推。不同在堆排序选择牌的方法更高效。堆排序选一次牌只花 o(logn),而直接选择排序选一次牌要花 o(n)。

写法一:空间复杂度 o(n)

1
2
3
4
5
6
7
8
9
10
function heapSort(arr) {
    let minheap = new Heap((x, y) => x - y, arr);
    
    const ans = [], len = arr.length;
    // 逐渐remove堆顶元素到结果数组ans
    for (let i = 0; i < len; i++) {
        ans.push(minheap.removeTop());
    }
    return ans;
}
  • 时间复杂度:o(nlogn)。removeTop()中包含一次heapifyDown(),时间复杂度为o(logn)。一共进行 n 次 removeTop(),因此堆排序为o(nlogn)。
  • 空间复杂度:o(n),结果数组 ans 额外开辟空间。

写法二:空间复杂度 o(1)

利用原数组空间,把堆顶元素直接放到原数组末尾,无需开辟新数组空间。相当于把原数组划分为两个部分:arr[0:i]arr[i+1:n],前半部分是堆的剩余元素,后半部分是已排好顺序的数组。

本文实现升序,需要大顶堆将目标数组堆化,每次removeTop()后都将堆顶元素放到堆的最末尾处,这样就能形成升序。

1
2
3
4
5
6
7
8
9
10
11
12
function heapSort(arr) {
    // arr与maxheap共用同一处空间
    // 将maxheap的this.size想象为一个始终指向堆末尾元素的指针
    // 随着while循环,堆逐渐remove top,this.size逐渐前移,而this.size后方的子数组即为当前排好顺序的数组arr
    let maxheap = new Heap((x, y) => y - x, arr);

    while (maxheap.size) { // 当堆不为空
        let top = maxheap.removeTop(); // 删除堆顶元素:堆内最大值
        arr[maxheap.size] = top; // 放到arr中
    }
    return arr;
}
  • 为什么是常数空间复杂度?

看下面的例子:(“|”前是堆内元素数组,后面是已排好序的arr数组)

1
2
3
4
5
6
7
8
// 最初 maxheap 内
[95, 34, 10, 6, 21, 2, 9, 2, 3, 7, 3, 1]
// 第一轮
[34, 21, 10, 6, 7, 2, 9, 2, 3, 1, 395]
// 第二轮
[21, 7, 10, 6, 3, 2, 9, 2, 3, 134, 95]
// 第三轮
[10, 7, 9, 6, 3, 2, 1, 2, 321, 34, 95]

参考资料

本文由作者按照 CC BY 4.0 进行授权

-

【基础算法】排序算法之插入排序(直接插入排序 + 希尔排序)