js 常见排序 冒泡排序、选择排序、插入排序、希尔排序、快速排序等5种排序

代码来源于 王红元(微博: coderwhy) 老师讲的数据结构算法

 <script>
    /*
      + 1. 冒泡排序
        + 冒泡排序算法相对其他排序运行效率较低, 但是在概念上它是排序算法中最简单的
          + 因此, 冒泡排序是在刚开始学习排序时, 最适合学习的一种排序方式
        + 冒泡排序的思路
          + 对未排序的各元素从头到尾依次比较相邻的两个元素大小关系
          + 如果左边的队员高, 则两个队员交换位置
          + 向右移动一个位置, 比较下面两个队员
          + 当走到最右端时, 最高的队员一定被放在了最右边
          + 按照这个思路, 从最左端重新开始, 这次走到倒数第二个位置的队员即可
          + 以此类推, 就可以将数据排序完成
        + 代码思路分析
          + 第一次找出最高的人放在最后, 我们需要两个两个数据项进行比较, 那么这个应该是一个循环操作
          + 第二次将次高的人找到放在倒数第二个位置, 也是两个比较, 只是不要和最后以比较(少了一次),
            但是前面的两个比较也是一个循环操作
          + 第三次...第四次...
          + 有发现规律吗? 这应该是一个循环中嵌套循环, 并且被嵌套的循环次数越来越少的
          + 根据这个分析,写出代码实现
        + 冒泡排序的效率
          + 冒泡排序的比较次数
            + 如果照上面的例子来说, 一共有 7 个数字, 那么每次循环时进行了几次的比较呢?
            + 第一次循环 6 次比较, 第二次5次比较, 第三次4次比较..., 直到最后一趟进行了一次比较
            + 对于 7 个数据项的比较次数: 6 + 5 + 4 + 3 + 2 + 1
            + 对于N个数据项呢? (N-1) + (N-2) + (N-3) + ... + 3 + 2 + 1 = N * (N-1) / 2
          + 通过大O表示法推导过程, 我们来推导一下 冒泡排序的大O形式:
            + N*(N - 1)/2 = N^2/2 - N/2, 根据规则2, 只保留最高阶向, 编程 N^2/2
            + N^2/2, 根据规则3, 去除常量, 编程N^2
            + 因此冒泡排序的大O表示法为 O(N^2)
          + 冒泡排序交换次数: 
            + 冒泡排序的交换次数是多少呢?
            + 如果有两次比较才需要交换一次(不可能每次比较都交换一次), 那么交换的次数为 N^2/4
            + 由于常量不算在大O表示法中, 因此, 我们可以认为交换次数的大O表示也是 O(N^2)

      
      
      + 2. 选择排序
        + 选择排序改进了冒泡排序
          + 将交换的次数由 O(N^2) 减少到 O(N)
          + 但是比较的次数依然是 O(N^2)
        + 选择排序思路
          + 选定一个索引位置, 然后和后面元素依次比较
          + 如果后面的队员, 小于第一个索引位置的队员, 则交换位置
          + 经过一轮的比较后, 可以确定第一个位置是最小的
          + 然后使用同样的办法把剩下的元素逐个比较即可
          + 可以看出选择排序, 第一轮会选出最小值, 第二轮会选出第二最小值, 直到最后
        + 选择排序的效率
          + 选择排序和冒泡排序的比较次数都是 N*(N - 1)/2, 也就是O(N^2)
        + 选择排序的交换次数
          + 选择排序每次进行选择的时候, 最多需要交换1次, 一共遍历多少次呢? N-1 次
          + 选择排序的交换次数只有N-1次, 用大O表示法就是O(N)
          + 所以选择排序通常认为在执行效率上是高于冒泡排序的

      + 3. 插入排序
        + 代码思路分析
          + 插入排序应该从 下标值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搞定
          + 按照上面的步骤, 依次找到最后一个元素, 整个数组排序完成
        + 代码解析:
          + 代码序号1: 获取数组的长度
          + 代码序号2: 外层循环, 从1位置开始, 因为0位置可以默认看成是有序的了
          + 代码序号3: 记录选出的i位置的元素, 保存在变量temp中, i 默认等于 j
          + 代码序号4: 内层循环:
            + 内层循环的判断 j-1 位置的元素和temp比较, 并且 j>0
            + 那么就将 j-1 位置的元素放在 j 位置
            + j 位置向前移
          + 代码序号5: 将目前选出的j位置放置temp元素
        + 插入排序的效率
          + 第一趟时, 需要的最多次数是1, 第二趟最多次数是2, 依次类推, 最后一趟是 N-1 次
          + 因此是 1+2+3 + ... + (N-2) + (N-1) = N*(N-1)/2
          + 然而每趟发现插入点之前, 平均只有全体数据项的一半需要进行比较
          + 我们可以除以2得到 N*(N-1)/4, 所以相对于选择排序, 其他比较次数是少了一半的
        + 插入排序的复制次数
          + 第一趟时, 需要的最多复制次数是 1, 第二趟最多次数是 2, 依次类推, 最后一趟是 N-1 次
          + 因此是: 1+2+3+...+(N-2)+(N-1) = N*(N-1)/2
        + 对于基本有序的情况
          + 对于已经有序或基本有序的数据来说, 插入排序要好很多
          + 当数据有序的时候, while循环的条件总是为false, 所以它便成了外循环中的一个简单语句, 执行 N-1 次
          + 在这种情况下, 算法运行需要 N(N) 放入时间, 效率相对来说会更高
          + 另外别忘了, 我们的比较次数是选择排序的一半, 所以这个算法的效率是高于选择排序的


      + 4. 希尔排序
        + 希尔排序是插入排序的一种高效的改进版, 并且效率比插入排序更快
        + 希尔排序的思路
          + 比如如下数字 81, 94, 11, 96, 12, 35, 17, 95, 28, 58, 41, 75, 15
          + 我们先让间隔为 5, 进行排序: (35, 81), (94, 17), (11, 95), (96, 28), (12, 58), (35, 41), (17, 75), (95, 15)
          + 排序后的新序列, 一定可以让数字离自己的正确位置更近一步
          + 我们再让间隔为3, 进行排序: (35,28,75,58,95),(17,12,15,81),(11,41,96,94)
          + 排序后的新序列, 一定可以让数字离自己的正确位置又近了一步
          + 最后, 我们让间隔为1, 也就是正确的插入排序
          + 这个时候数字都离自己的位置更近, 那么需要赋值的次数一定会减少很多
        + 希尔原稿的做法
          + 选择合适的增量
            + 在希尔排序的原稿中, 他建议的初始间距是 N/2, 简单的把每趟排序分成两半
            + 也就是说, 对于N = 100 的数组, 增量间隔序列为: 50, 25, 12, 6, 3, 1
            + 这个方法的好处是不需要在开始排序前为找合适的增量而进行任何的计算.
          + Hibbard 增量序列
            + 增量的算法为 2^k-1 也就是为 1, 3, 5, 7, 9 ...等等
            + 这种增量的最坏复杂度为 O(N^(3/2)), 猜想的平均复杂度为 O(N/(5/4)), 目前尚未被证明
          + Sedgewick 增量序列
            + {1,5,19,41,109, ...}, 该序列中的项或者是  94^i - 9*2^i + 1 或者是 4^i - 32^i + 1
            + 这种增量的最坏复杂度为 O(N^(4/3)), 平均复杂度为 O(N^(7/6)), 但是均未被证明
        + 代码解析
          + 代码序号1: 获取数组的长度
          + 代码序号2: 计算第一次的间隔, 我们按照希尔提出的间隔实现
          + 代码序号3: 增量不断减小, 大于0就继续改变增量
          + 代码序号4: 实际上就是实现了插入排序
            + 代码序号 4.1: 保存临时变量, j位置从i开始, 保存该位置的值到变量 temp 中
            + 代码序号4.2: 内层循环, j > gap - 1 并且 temp > this.array[j-gap], 那么就进行赋值
            + 代码序号4.3: 将 j 位置设置为变量 temp
            + 代码序号5: 每次 while 循环后都重新计算新的间隔 
        + 希尔排序的效率
          + 希尔排序的效率跟增量是有关系的
          + 但是, 它的效率证明非常困难, 甚至某些增量的效率到目前依然没有被证明出来
          + 但是经过统计, 希尔排序使用原始增量, 最坏的情况下时间的复杂度为 O(N^2), 通常情况下都要好于 O(N^2)
        + 总之,我们使用希尔排序的大多数情况下效率都高于简单排序
          + 这个可以通过统计排序算法的时间来证明
          + 甚在合适的增量和某些数量N的情况下, 还好于快速排序
      + 5. 快速排序
        + 快速排序几乎可以说是目前所有排序算法中, 最快的一种排序算法
          + 当然, 没有任何一种算法是在任意情况下都是最优的
          + 比如希尔排序确实在某些情况下可能好于快速排序
          + 但是大多数情况下, 快速排序还是比较好的选择
        + 快速排序的重要性
          + 如果有一天你面试的时候, 让你写一个排序算法
          + 你可以洋洋洒洒的写出多个排序算法, 但是如果其中没有快速排序
          + 那么证明你对排序算法也只是浅尝辄止, 并没有深入研究过
          + 因为快速排序可以说是排序算法中最常见的, 无论是 C++ 的STL中, 还是Java的SDK中其实都能找到它的影子
          + 快速排序也被列为20世纪十大算法之一  
        + 希尔排序相当于插入排序的升级版, 快速排序其实就是最慢的冒泡排序的升级版
          + 冒泡排序需要经过很多次交换, 才能在一次循环中, 将最大值放在正确位置, 并且钙元素之后不需要任何移动
        + 和冒泡排序的不同之处? 
          + 选择的65可以一次性将它放在最正确的位置, 之后不需要任何移动. 
          + 需要从开始位置两个两个比较, 如果第一个就是最大值, 它需要一直向后移动, 直到走到最后. 
          + 也就是即使已经找到了最大值, 也需要不断继续移动最大值. 而插入排序对数字的定位是一次性的.   
        + 快速排序的重要思想是 分而治之
          + 比如下面有这样一顿数字需要排序: 13、81、92、43、65、31、57、26、75、0
          + 第一步: 从其中选出了65 (其实可以是选出任意的数字, 我们以65举例子)
          + 第二步: 我们通过算法: 将所有小于65的数字放在65的左边, 所有大于65的数字放在65的右边
          + 第三步: 递归的处理左边的数据(比如选31来处理左侧), 递归处理右边的数据(比如选择75来处理右侧) 
        + 快速排序的枢纽
          + 在快速排序中有一个很重要的步骤就是选取枢纽(pivot 也有人称为主元)
            + 如何选择才是最合适的枢纽呢
          + 一种方案是直接选择第一个元素作为枢纽. 
            + 但第一个作为枢纽在某些情况下, 效率并不是特别高. 
          + 另一种方案是使用随机数:
            + 随机pivot? 但是随机函本身就是一个耗性能的操作
          + 另一种比较优秀的解决方案: 取 头、中、尾 的中位数
            + 例如 8、12、3 的中位数就是 8  
        + 快速排序的最坏情况效率
          + 什么情况下会有最坏的效率呢? 就是每次选择的按钮都是最左边或者最后边的
          + 那么效率等同于冒泡排序. 
          + 而我们的例子可能有最坏的情况吗? 是不可能的, 因为我们是选择三个值对的中位值
        + 快速排序的平均效率: 
          + 快速排序的平均效率是O(N*logN)
          + 虽然其他某些算法的效率也可以达到O(N*logN), 但是快速排序是最好的.             
    */



    // 创建列表类
    function ArrayList () {
      // 属性
      this.array = []

      // 方法
      // 将数据可以插入到数组中的方法
      ArrayList.prototype.insert = function (item) {
        this.array.push(item)
      }
      // toString
      ArrayList.prototype.toString = function () {
        return this.array.join('-')
      }
      // 交换两个位置的数据
      ArrayList.prototype.swap = function (m, n) {
        var temp = this.array[m]
        this.array[m] = this.array[n]
        this.array[n] = temp
      }

      // 实现排序算法
      // 冒泡排序
      ArrayList.prototype.bubbleSort = function () {
        // 获取数组的长度
        let length = this.array.length
        // 外层循环应该让 i 依次减少, 因此我们这里使用了反向的遍历
        for (let i = length - 1; i >= 0; i--) {
          // 内层循环我们使用 j < i , 因为上面的 i 在不断减小, 这样就可以控制内层循环的次数
          for (let j = 0; j < i; j++) {
            // 对比相邻两个数据的大小
            if (this.array[j] > this.array[j + 1]) {
              // 比较两个数据项的大小, 如果前面的比较大, 那么就进行交换
              this.swap(j, j + 1)
            }
          }
        }

      }

      // 选择排序
      ArrayList.prototype.selectSort = function () {
        // 获取数组的长度
        let length = this.array.length
        // 2 外循环: 从 0 位置开始取数据
        for (let j = 0; j < length - 1; j++) {
          var min = j
          // 内层循环: 从 i + 1 位置开始, 和后面的数据进行比较
          for (let i = min + 1; i < length; i++) {
            if (this.array[min] > this.array[i]) {
              min = i
            }
          }
          this.swap(min, j)
        }
      }

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

      // 希尔排序
      ArrayList.prototype.shellSort = function () {
        // 1.获取数组的长度
        var length = this.array.length
        // 2.初始化的增量(gap -> 间隔/间隙)
        var gap = Math.floor(length / 2)
        // 3.while循环(gap不断的减小)
        while (gap >= 1) {
          // 4. 以gap作为间隔, 进行分组, 对分组进行插入排序
          for (var i = gap; i < length; i++) {
            var temp = this.array[i]
            var j = i
            while (this.array[j - gap] > temp && j > gap - 1) {
              this.array[j] = this.array[j - gap]
              j -= gap
            }
            // 5. 将 j 位置的元素赋值temp
            this.array[j] = temp
          }
          // 6. 增量变化 / 2
          gap = Math.floor(gap / 2)
        }
      }

      // 快速排序
      // 1. 选择枢纽
      ArrayList.prototype.median = function (left, right) {
        // 1.1 取出中间位置
        var center = Math.floor((left + right) / 2)
        // 1.2 判断大小, 并且进行交换
        if (this.array[left] > this.array[center]) {
          this.swap(left, center)
        }
        if (this.array[left] > this.array[right]) {
          this.swap(left, right)
        }
        if (this.array[center] > this.array[right]) {
          this.swap(center, right)
        }
        // 1.3 将center换到 right -1 的位置
        this.swap(right - 1, center)
        return this.array[right - 1]
      }
      // 2. 快速排序实现
      ArrayList.prototype.quickSort = function (left, right) {
        let length = this.array.length
        this.quick(0, length - 1)
      }
      // 3. 递归实现
      ArrayList.prototype.quick = function (left, right) {
        // 3.1 结束递归条件
        if (left >= right) return
        // 3.2 获取枢纽
        let pivot = this.median(left, right)
        // 3.3 定义变量, 用于记录当找到的位置
        let i = left
        let j = right - 1
        // 3.4 开始进行交换
        while (true) {
          while (this.array[++i] < pivot) { }
          while (this.array[--j] > pivot) { }
          if (i < j) {
            this.swap(i, j)
          } else {
            break
          }
        }
        // 3.5 将枢纽放置在正确的位置, i 的位置
        this.swap(i, right - 1)
        // 3.6 分而治之
        this.quick(left, i - 1)
        this.quick(i + 1, right)
      }
    }
    // 测试类
    var list = new ArrayList()
    // 插入元素
    list.insert(66)
    list.insert(88)
    list.insert(12)
    list.insert(87)
    list.insert(100)
    list.insert(5)
    list.insert(566)
    list.insert(23)
    list.insert(1)
    list.insert(1987)
    list.insert(236)
    list.insert(974)
    list.insert(83)
    list.insert(13)
    list.insert(726)
    list.insert(267)
    list.insert(10)
    // alert(list)
    // 验证冒泡排序
    console.time('A')
    // list.bubbleSort() // 0.0859375 ms
    // list.selectSort() // 0.078857421875 ms
    // list.insertSort() // 0.064208984375 ms
    // list.shellSort() // 0.079833984375 ms
    list.quickSort() //  0.073974609375 ms
    console.timeEnd('A') //0.0859375 ms
    // alert(list)
  </script>
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值