数据结构和算法(十二)之简单排序

数据结构和算法(十二)之简单排序

排序算法有很多: 冒泡排序 / 选择排序 / 插入排序 / 归并排序 / 计数排序 (counting sort)/ 基数排序 (radix sort)/ 希尔排序 / 堆排序 / 桶排序.

我们这里不一一列举它们的实现思想, 而是选择几个简单排序和高级排序.(后续有机会给大家视频讲解)

简单排序: 冒泡排序 - 选择排序 - 插入排序

高级排序: 希尔排序 - 快速排序

其他排序的理论和思想, 大家可以自行学习.

一. 排序介绍

我们先对排序有个简单的认识, 然后开始介绍几种简单排序.

排序介绍

  • 一旦我们将数据放置在某个数据结构中存储起来后 (比如数组), 就可能根据需求对数据进行不同方式的排序
    • 比如对姓名按字母排序
    • 对学生按年龄排序
    • 对商品按照价格排序
    • 对城市按照面积或者人口数量排序
    • 对恒星按照大小排序
    • 等等
  • 由于排序非常重要而且可能非常耗时, 所以它已经成为一个计算机科学中广泛研究的课题, 而且人们已经研究出一套成熟的方案来实现排序.
    • 但是, 我们学习已有的排序方法是非常有必要的.

如何排序?

  • 需求: 对一组身高不等的 10 个人进行排序
  • 人来排序:
    • 如果是人来排序事情会非常简单, 因为人只要扫过去一眼就能看出来谁最高谁最低.
    • 然后让最低 (或者最高) 的站在前面, 其他人依次后移.
    • 按照这这样的方法. 依次类推就可以了.
  • 计算机来排序:
    • 计算机有些笨拙, 它只能执行指令. 所以没办法一眼扫过去.
    • 计算机也很聪明, 只要你写出了正确的指令, 可以让它帮你做无数次类似的事情而不用担心出现错误.
    • 并且计算机排序也无需担心数据量的大小.(想象一样, 让人排序 10000 个, 甚至更大的数据项你还能一眼扫过去吗?)
    • 人在排序时不一定要固定特有的空间, 他们可以相互推推嚷嚷就腾出了位置, 还能互相前后站立.
    • 但是计算机必须有严密的逻辑和特定的指令.
  • 计算机排序的特点:
    • 计算机不能像人一样, 一眼扫过去这样通览所有的数据.
    • 它只能根据计算机的比较操作原理, 在同一个时间对两个队员进行比较.
    • 在人类看来很简单的事情, 计算机的算法却不能看到全景, 因此它只能一步步解决具体问题和遵循一些简单的规则.
  • 简单算法的主要操作:
    • 比较两个数据项.
    • 交换两个数据项, 或者复制其中一项.
    • 但是, 每种算法具体实现的细节有所不同.

创建列表

  • 在开始排序前, 我们先来创建一个列表封装我们的数据项.

    // 封装ArrayList
    function ArrayList() {
        this.array = []
    
        ArrayList.prototype.insert = function (item) {
            this.array.push(item)
        }
    
        ArrayList.prototype.toString = function () {
            return this.array.join()
        }
    }
    
  • 初始化数据项

    // 初始化数据项
    let list = new ArrayList()
    
    list.insert(3)
    list.insert(6)
    list.insert(4)
    list.insert(2)
    list.insert(11)
    list.insert(10)
    list.insert(5)
    
    alert(list)
    

二. 冒泡排序

冒泡排序算法相对其他排序运行效率较低, 但是在概念上它是排序算法中最简单的.

因此, 冒泡排序是在刚开始学习排序时, 最适合学习的一种排序方式.

冒泡排序的思路

  • 冒泡排序的思路:

    • 对未排序的各元素从头到尾依次比较相邻的两个元素大小关系
    • 如果左边的队员高, 则两队员交换位置
    • 向右移动一个位置, 比较下面两个队员
    • 当走到最右端时, 最高的队员一定被放在了最右边
    • 按照这个思路, 从最左端重新开始, 这次走到倒数第二个位置的队员即可.
    • 依次类推, 就可以将数据排序完成
  • 冒泡排序的图解:

  • 思路再分析:

    • 第一次找出最高人放在最后, 我们需要两个两个数据项进行比较, 那么这个应该是一个循环操作.
    • 第二次将次高的人找到放在倒数第二个位置, 也是两个比较, 只是不要和最后一个比较 (少了一次), 但是前面的两个两个比较也是一个循环操作.
    • 第三次… 第四次…
    • 有发现规律吗? 这应该是一个循环中嵌套循环, 并且被嵌套的循环次数越来越少的.
    • 根据这个分析, 你能写出代码实现吗?

冒泡排序的实现

  • 冒泡排序的实现:

    ArrayList.prototype.bubbleSort = function () {
        // 1.获取数组的长度
        let length = this.array.length
    
        // 2.反向循环, 因此次数越来越少
        for (let i = length - 1; i >= 0; i--) {
            // 3.根据i的次数, 比较循环到i位置
            for (let j = 0; j < i; j++) {
                // 4.如果j位置比j+1位置的数据大, 那么就交换
                if (this.array[j] > this.array[j+1]) {
                    // 交换
                    this.swap(j, j+1)
                }
            }
        }
    }
    
    ArrayList.prototype.swap = function (m, n) {
        let temp = this.array[m]
        this.array[m] = this.array[n]
        this.array[n] = temp
    }
    
  • 代码解析:

    • 代码序号 1: 获取数组的长度.
    • 代码序号 2: 我们现在要写的外层循环, 外层循环应该让 i 依次减少, 因此我们这里使用了反向的遍历.
    • 代码需要 3: 内层循环, 内层循环我们使用 j < i. 因为上面的 i 在不断减小, 这样就可以控制内层循环的次数.
    • 代码需要 4: 比较两个数据项的大小, 如果前面的大, 那么就进行交换.
  • 代码图解流程:

    img

  • 测试代码:

    // 测试冒泡排序
    list.bubbleSort()
    alert(list) // 2,3,4,5,6,10,11
    

冒泡排序的效率

  • 冒泡排序的比较次数:
    • 如果按照上面的例子来说, 一共有 7 个数字, 那么每次循环时进行了几次的比较呢?
    • 第一次循环 6 次比较, 第二次 5 次比较, 第三次 4 次比较… 直到最后一趟进行了一次比较.
    • 对于 7 个数据项比较次数: 6 + 5 + 4 + 3 + 2 + 1
    • 对于 N 个数据项呢? (N - 1) + (N - 2) + (N - 3) + … + 1 = N * (N - 1) / 2
  • 大 O 表示法:
    • 大 O 表示法是描述性能和复杂度的一种表示方法.
    • 推导大 O 表示法通常我们会使用如下规则:
      • 用常量 1 取代运行时间中的所有加法常量
      • 在修改后的运行次数函数中, 只保留最高阶项
      • 如果最高阶项存在并且不是 1, 则去除与这个项相乘的常数.
  • 通过大 O 表示法推到过程, 我们来推到一下冒泡排序的大 O 形式.
    • N * (N - 1) / 2 = N²/2 - N/2, 根据规则 2, 只保留最高阶项, 编程 N² / 2
    • N² / 2, 根据规则 3, 去除常量, 编程 N²
    • 因此冒泡排序的大 O 表示法为 O(N²)
  • 冒泡排序的交换次数:
    • 冒泡排序的交换次数是多少呢?
    • 如果有两次比较才需要交换一次 (不可能每次比较都交换一次.), 那么交换次数为 N² / 4
    • 由于常量不算在大 O 表示法中, 因此, 我们可以认为交换次数的大 O 表示也是 O(N²)

三. 选择排序

选择排序改进了冒泡排序, 将交换的次数由 O(N²) 减少到 O(N), 但是比较的次数依然是 O(N²)

选择排序的思路

  • 选择排序的思路:

    • 选定第一个索引位置,然后和后面元素依次比较
    • 如果后面的队员, 小于第一个索引位置的队员, 则交换位置
    • 经过一轮的比较后, 可以确定第一个位置是最小的
    • 然后使用同样的方法把剩下的元素逐个比较即可
    • 可以看出选择排序,第一轮会选出最小值,第二轮会选出第二小的值,直到最后
  • 选择排序的图解

  • 思路再分析:

    • 选择排序第一次将第 0 位置的人取出, 和后面的人 (1, 2, 3…) 依次比较, 如果后面的人更小, 那么就交换.
    • 这样经过一轮之后, 第一个肯定是最小的人.
    • 第二次将第 1 位置的人取出, 和后面的人 (2, 3, 4…) 依次比较, 如果后面的人更小, 那么就交换.
    • 这样经过第二轮后, 第二个肯定是次小的人.
    • 第三轮… 第四轮… 直到最后就可以排好序了. 有发现规律吗?
    • 外层循环依次取出 0-1-2…N-2 位置的人作为 index(N-1 不需要取了, 因为只剩它一个了肯定是排好序的)
    • 内层循环从 index+1 开始比较, 直到最后一个.
    • 经过分析, 你能写出最终的算法吗?

选择排序的实现

  • 选择排序的实现:

    ArrayList.prototype.selectionSort = function () {
        // 1.获取数组的长度
        let length = this.array.length
    
        // 2.外层循环: 从0位置开始取出数据, 直到length-2位置
        for (let i = 0; i < length - 1; i++) {
            // 3.内层循环: 从i+1位置开始, 和后面的内容比较
            var min = i
            for (let j = min + 1; j < length; j++) {
                // 4.如果i位置的数据大于j位置的数据, 那么记录最小的位置
                if (this.array[min] > this.array[j]) {
                    min = j
                }
            }
            // 5.交换min和i位置的数据
            this.swap(min, i)
        }
    }
    
  • 代码解析:

    • 代码序号 1: 依然获取数组的长度.
    • 代码序号 2: 外层循环, 我们已经讲过, 需要从外层循环的第 0 个位置开始, 依次遍历到 length - 2 的位置.
    • 代码序号 3: 先定义一个 min, 用于记录最小的位置, 内层循环, 内层循环是从 i+1 位置开始的数据项, 和 i 位置的数据项依次比较, 直到 length-1 的数据项.
    • 代码序号 4: 如果比较的位置 i 的数据项, 大于后面某一个数据项, 那么记录最小位置的数据.
    • 代码序号 5: 将 min 位置的数据, 那么 i 位置的数据交换, 那么 i 位置就是正确的数据了.
    • 注意: 这里的交换是基于之前的交换方法, 这里直接调用即可.
  • 代码图解流程:

  • 测试代码:

    // 测试选择排序
    list.selectionSort()
    alert(list) // 2,3,4,5,6,10,11
    

选择排序的效率

  • 选择排序的比较次数:
    • 选择排序和冒泡排序的比较次数都是 N*(N-1)/2, 也就是 O(N²).
  • 选择排序的交换次数:
    • 选择排序的交换次数只有 N-1 次, 用大 O 表示法就是 O(N).
    • 所以选择排序通常认为在执行效率上是高于冒泡排序的.

四. 插入排序

插入排序是简单排序中效率最好的一种.

插入排序也是学习其他高级排序的基础, 比如希尔排序 / 快速排序, 所以也非常重要.

插入排序的思路

  • 局部有序:

    • 插入排序思想的核心是局部有序. 什么是局部有序呢?
    • 比如在一个队列中的人, 我们选择其中一个作为标记的队员. 这个被标记的队员左边的所有队员已经是局部有序的.
    • 这意味着, 有一部门人是按顺序排列好的. 有一部分还没有顺序.
  • 插入排序的思路:

    • 从第一个元素开始,该元素可以认为已经被排序
    • 取出下一个元素,在已经排序的元素序列中从后向前扫描
    • 如果该元素(已排序)大于新元素,将该元素移到下一位置
    • 重复上一个步骤,直到找到已排序的元素小于或者等于新元素的位置
    • 将新元素插入到该位置后, 重复上面的步骤.
  • 插入排序的图解

  • 思路再分析:

    • 插入排序应该从下标值 1 开始 (因为 0 位置默认可以被认为是有序的)
    • 从 1 位置开始取出元素, 并且判断该元素的大小和 0 位置进行比较, 如果 1 位置元素小于 0 位置元素, 那么交换, 否则不交换.
    • 上面步骤执行完成后, 0 - 1 位置已经排序好.
    • 取出 2 位置的元素, 和 1 位置进行比较:
      • 如果 2 位置元素大于 1 位置元素, 说明 2 位置不需要任何动作. 0 - 1 - 2 已经排序好.
      • 如果 2 位置元素小于 1 位置元素, 那么将 1 移动到 2 的位置, 并且 2 继续和 0 进行比较.
      • 如果 2 位置元素大于 0 位置的元素, 那么将 2 位置放置在 1 的位置, 排序完成. 0 - 1 - 2 搞定.
      • 如果 2 位置元素小于 1 位置的元素, 那么将 0 位置的元素移动到 1 位置, 并且将 2 位置的元素放在 0 位置, 0 - 1 - 2 搞定.
    • 按照上面的步骤, 依次找到最后一个元素, 整个数组排序完成.
    • 经常上面的分析, 你能转化成对应的代码吗?

插入排序的实现

  • 插入排序的实现:

    ArrayList.prototype.insertionSort = function () {
        // 1.获取数组的长度
        let length = this.array.length
    
        // 2.外层循环: 外层循环是从1位置开始, 依次遍历到最后
        for (let i = 1; i < length; i++) {
            // 3.记录选出的元素, 放在变量temp中
            let j = i
            let temp = this.array[i]
    
            // 4.内层循环: 内层循环不确定循环的次数, 最好使用while循环
            while (j > 0 && this.array[j-1] > temp) {
                this.array[j] = this.array[j-1]
                j--
            }
    
            // 5.将选出的j位置, 放入temp元素
            this.array[j] = temp
        }
    }
    
  • 代码解析

    • 代码序号 1: 获取数组的长度.
    • 代码序号 2: 外层循环, 从 1 位置开始, 因为 0 位置可以默认看成是有序的了.
    • 代码序号 3: 记录选出的 i 位置的元素, 保存在变量 temp 中. i 默认等于 j
    • 代码序号 4: 内层循环
      • 内层循环的判断 j - 1 位置的元素和 temp 比较, 并且 j > 0.
      • 那么就将 j-1 位置的元素放在 j 位置.
      • j 位置向前移.
    • 代码序号 5: 将目前选出的 j 位置放置 temp 元素.
  • 代码的图解流程 (来自维基百科):

  • 测试代码:

    // 测试插入排序
    list.insertionSort()
    alert(list) // 2,3,4,5,6,10,11
    

插入排序的效率

  • 插入排序的比较次数:
    • 第一趟时, 需要的最多次数是 1, 第二趟最多次数是 2, 依次类推, 最后一趟是 N-1 次.
    • 因此是 1 + 2 + 3 + … + N - 1 = N * (N - 1) / 2.
    • 然而每趟发现插入点之前, 平均只有全体数据项的一半需要进行比较.
    • 我们可以除以 2 得到 N * (N - 1) / 4. 所以相对于选择排序, 其他比较次数是少了一半的.
  • 插入排序的复制次数:
    • 第一趟时, 需要的最多复制次数是 1, 第二趟最多次数是 2, 依次类推, 最后一趟是 N-1 次.
    • 因此是 1 + 2 + 3 + … + N - 1 = N * (N - 1) / 2.
  • 对于基本有序的情况
    • 对于已经有序或基本有序的数据来说, 插入排序要好很多.
    • 当数据有序的时候, while 循环的条件总是为假, 所以它变成了外层循环中的一个简单语句, 执行 N-1 次.
    • 在这种情况下, 算法运行至需要 N(N) 的时间, 效率相对来说会更高.
    • 另外别忘了, 我们的比较次数是选择排序的一半, 所以这个算法的效率是高于选择排序的.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

yige001

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值