一些排序算法的思想和实现

507 阅读6分钟
最近公司不忙,看了一下排序算法,总结起来计算机实现排序就是:基于比较两个数据项,然后交换位置实现的。

以下分别介绍了冒泡排序,选择排序,插入排序,希尔排序和快速排序。

冒泡排序

  •  从头到尾依次比较相邻的两个元素大小关系
  • 如果前一个元素比后一个元素大,则交换位置
  • 一轮比较结束时,最大的元素一定被放在数组的末尾
  • 重复前面的操作,这次只用比较到数组倒数第二个元素
  • 以此类推

bubble.png

// 冒泡排序
function bubbleSort(arr) {
      var length = arr.length;
      // 每一轮比较的长度都递减
      for (var i = length - 1; i > 0; i--) {  
        // 从头到尾依次比较
        for (var j = 0; j < i; j++) { 
          // 如前一个比后一个大则交换位置
          if (arr[j] > arr[j + 1]) {
            var temp = arr[j];
            arr[j] = arr[j + 1];
            arr[j + 1] = temp;
          }
        }  
      }
      return arr;
    }

选择排序

  • 从数组下标为0位置的元素开始直到数组的末尾
  • 找出这些元素中最小的值在哪个位置上
  • 将最小的值交换到0位置上
  • 重复前面的操作,从下标为1位置的元素开始
  • 依此类推

selection.png

// 选择排序
function selectionSort(arr) {
  var length = arr.length;
  for (var i = 0; i < length - 1; i++) {
    // 找出每一轮的最小值,保存在下标为min的位置
    var min = i;
    for (var j = min + 1; j < length; j++) {
      if (arr[min] > arr[j]) {
        min = j;  
      }
    }
    // 将min交换到数列的起始位置
    var temp = arr[i];
    arr[i] = arr[min];
    arr[min] = temp;
  }
  return arr;
}

插入排序

假设数组成员的左边(前面)是已经排好队的序列,这时只需逐个将数组的成员插入到序列的合适位置即可。

  • 从数组的第二个元素开始,此时因为左边只有一个元素,可以认为左边是一个已经排好队的序列
  • 将第二个元素与第一个比较,如果第一个元素大,则把第一个元素向右移一位,第二个元素放到第一个元素的位置;否则保持位置不变
  • 取出第三个元素,与左边已经排好的序列(第一、二个元素)从右到左依次比较,如果序列的元素大于第三个元素,则将该元素向右移一位,直到序列的元素不大于第三个元素或者已经比较到了最左边,将第三个元素插入到最后一次被移动元素的位置
  • 取出第四个元素,重复上面一步
  • 以此类推

insertion.png

 //  插入排序
function insertionSort(array) {
  var length = array.length;
  // 从第二个元素开始
  for (var i = 1; i < length; i++) {  
    // 保存每一轮比较用来比较的值和下标
    var j = i;
    var temp = array[i]; 
    // 排好队的序列从右到左依次比较,如果序列的元素较大则右移一位
    while (j > 0 && array[j - 1] > temp) {
      array[j] = array[j - 1];
      j --;  
    }  
    // 将用来比较的值插入到合适的位置
    array[j] = temp;
  }  
  return array;
}

希尔排序

希尔排序是基于插入排序,只不过不是一开始就依次逐个地去插入,而是先根据一定的间隔将数列分成若干组,在组内通过插入排序将顺序排好,再将间隔缩小,又排好序,直至间隔最小为1。

shell.png

// 希尔排序
function shellSort(array) {
  var length = array.length;
  // 初始化间隔为数组长度的一半
  var gap = Math.floor(length / 2);
  // 间隔最小是1
  while (gap > 0) {
    // 从第二轮间隔处开始取值,与前面的数进行比较
    for (var i = gap; i < length; i++) {
      // 保存每一轮用来比较的值和下标
      var j = i; 
      var temp = array[i]; 
      // 进行插入排序的比较,如果左边的值较大则右移一位(在组内) 
      while(j + 1 > gap && array[j - gap] > temp) {
        array[j] = array[j - gap];
        j -= gap;
      }
      // 将用来比较的值插入到合适的位置
      array[j] = temp
    }
    // 间隔每次减半,且最小值为1
    gap = Math.floor(gap / 2);
  }
  return array
}

快速排序

快速排序的核心是选取数组中的一个值,称之为枢纽(pivot),将小于枢纽的值放在枢纽的左边,大于的放在右边,此时枢纽的位置可以不动了。再分别将枢纽的左边和右边使用此规则重复排序,以此类推,直至数组遍历结束。

quick.png

class ArrayList {
  array;
  constructor(list) {
    this.array = list;
  }
  // 交换两个元素的位置
  swap(m, n) {
    let temp = this.array[m]
    this.array[m] = this.array[n]
    this.array[n] = temp
  }
  // 获取枢纽(将数组的头,中间,尾按照大小顺序排好,枢纽选取中间的值)
  getPivot(left, right) {  
      // 1.求出中间的位置
      let center = Math.floor((left + right) / 2)

      // 2.判断并且进行交换
      if (this.array[left] > this.array[center]) {
          this.swap(left, center)
      }
      if (this.array[center] > this.array[right]) {
          this.swap(center, right)
      }
      // 此时right位置的值已经确定,只需再次比较left和center
      if (this.array[left] > this.array[center]) {
          this.swap(left, center)
      }

      // 3.巧妙的操作: 将pivot移动到right - 1的位置,
      // 可用于后面交换的时候, pivot的值不需要移动来移动去
      // 选定位置后, 直接交换到正确的位置即可
      this.swap(center, right - 1)

      // 4.返回pivot
      return this.array[right - 1]
  }

  quickSort () {
    this.quickSortRec(0, this.array.length - 1)
  }

  quickSortRec (left, right) {
      // 0.递归结束条件
      if (left >= right) return

      // 1.获取枢纽
      let pivot = this.getPivot(left, right);
      // 保存左、右开始时的位置
      let i = left  
      let j = right - 1  
      // 2.2.循环查找位置
      while (i < j) {
        // 从两头同时开始查找,如果左边的值大于枢纽,并且右边的值小于枢纽,则交换位置
          while (this.array[++i] < pivot) { }
          while (this.array[--j] > pivot) { }
          if (i < j) {
              this.swap(i, j)
          } else {
              // 如果i > j,则遍历结束,退出循环
              break
          }
      }
      // 3.将枢纽放在正确的位置(此时枢纽左边都是较小的值,枢纽右边都是较大的值)
      this.swap(i, right - 1);
      // 4.递归调用,分别再排枢纽左边和右边
      this.quickSortRec(left, i - 1) 
      this.quickSortRec(i + 1, right) 
  }
}
let list = new ArrayList([11,14,6,3,8,5,2,8,10]);
list.quickSort();
console.log(list.array)  // [2, 3, 5, 6, 8, 8, 10, 11, 14]

效率分析比较

注:详细的效率分析非常复杂,在此仅作简单分析(比较次数和交换次数),或者给出结论。

比较次数:进行数据比较的次数。
交换次数:数据交换的次数。

  • 如冒泡排序中,假设数组长度为7
  • 则第一次循环6次比较, 第二次5次比较, 第三次4次比较....直到最后一趟进行了一次比较
  • 7个数据项比较次数: 6 + 5 + 4 + 3 + 2 + 1
  • 则N个数据项的比较次数:N * (N - 1) / 2

冒泡排序 VS 选择排序
冒泡排序和选择排序的比较次数都是N * (N - 1) / 2。冒泡排序假设两次比较需要交换一次,则交换次数为N * (N - 1) / 4;而选择排序的仅有N-1次,所以可知选择排序的效率大于冒泡排序。

选择排序 VS 插入排序
在插入排序中,因为在比较时只要找到了一个不大于的值,就不再进行后面的比较,所以比较次数最多为N * (N - 1) / 2。交换次数最多也为N * (N - 1) / 2。 对于基本有序的情况,插入排序的效率大于选择排序。

希尔排序
多数情况下,希尔排序的效率高于前面3种。

快速排序
多数情况下,快速排序的效率高于前面4种。

在javascript中的应用

js中sort()方法可对数组元素进行排序,其底层实现在chrome V8引擎array.js 源码可知,数组长度小于等于 22 的用插入排序,其它的用快速排序,见下图:

sort_source.png

参考链接:www.jianshu.com/p/f1f2dc978…
参考链接:www.jianshu.com/p/3c2184320…