算法学习笔记:排序算法(二)

上一篇中已经介绍了几个简单的排序算法,这一篇文章我将继续向你们介绍排序算法相关的内容,本篇的会介绍希尔排序、快速排序、归并排序以及分治算法的思想,但愿经过本文章可以加深你们对排序算法的理解。算法

希尔排序

希尔排序又叫缩小增量排序,希尔排序的主要思想是使数组中任意相隔h的元素都是有序的,h就是希尔增量,实现的希尔排序的方法就是:对相隔h的元素进行分组,对每组进行使用插入排序,使子序列变成有序序列,增量逐渐递减一直到1为止,在例子中我将h增量设为array.length/2,递减的过程就是不断除以2,是否是被个人解释弄的有点晕,让咱们先来看一个演示过程理解一下:shell

clipboard.png

如图所示,一共15个元素,增量就是15/2为7,第一轮的分组即为[2, 26, 48],[44, 26],[38, 2],[5, 46],[47, 4],[15, 19],[36, 50],而后进行插入排序,获得新的序列[ 3, 27, 2, 5, 4, 15, 36, 26, 44, 38, 46, 47, 19, 50, 48 ],而后在进行分组,增量为7/2为3:数组

clipboard.png

第二轮分组[3, 5, 36, 2, 19],[44, 47, 26, 46, 50], [38, 15, 27, 4, 48],而后进行插入排序,获得序列[ 3, 4, 2, 5, 26, 15, 19, 27, 44, 36, 46, 47, 38, 50, 48 ],而后再进行分组,增量为3/2为1,再插入排序获得的就是一个有序序列了,最好让咱们来看具体的代码实现:spa

function shellSort(arr) {
  var n = arr.length
  for (var gap = parseInt(n/2); gap > 0; gap=parseInt(gap/2)) {
    for (var i=gap; i<arr.length; i++) {
      var j = i - gap, temp = arr[i]
      while (arr[j] > temp) {
        arr[j+gap] = arr[j]
        j = j - gap
      }
      arr[j+gap] = temp
    }
  }
  console.log(arr)
}
shellSort([3,44,38,5,47,15,36,26,27,2,46,4,19,50,48])

其实希尔排序也不难,只要按照上面的分解图示一步一步的理解去编写,相信你们都能写得出来,上面这种形式的增量设置就是二分的形式设置,而后插入排序,还有一种希尔排序的写法就是自定义增量,而后子序列里的元素两两比较,来看具体代码:设计

function shellSort(arr) {
  var n = arr.length, gap = 1
  while (gap < n / 3) {
    gap = gap * 3 + 1
  }
  for (gap; gap > 0; gap=parseInt(gap/3)) {
    for (var i=gap; i<arr.length; i++) {
      for (var j = i - gap;j >= 0; j-=gap) {
        if (arr[j] > arr[j+gap]) {
          var temp = arr[j+gap]
          arr[j+gap] = arr[j]
          arr[j] = temp
        }
      }
    }
  }
  console.log(arr)
}
shellSort([3,44,38,5,47,15,36,26,27,2,46,4,19,50,48])

希尔排序更高效的缘由在于它权衡了子数组的规模和有序性,各个子数组都很短,排序以后的子数组都是部分有序的,所以在希尔排序的效率要比插入排序高。code

分治法

在介绍归并排序和快速排序有必要先介绍一下分治法相关的内容,为何呢?由于归并排序和快速排序都是分治法的典型应用。
分治法的设计思路就是:将一个大问题分解成若干个规模娇小的相同子问题,这些子问题互相独立且与原问题形式相同,递归地解决这些子问题,而后将各个子问题的解合并获得原问题的解
分治法所能解决的问题通常有以下几个特色:blog

  1. 把该问题缩小到必定规模就能够容易解决
  2. 该问题能够被分解为若干个规模较小的相同问题
  3. 利用该问题的子问题的解向上合并能够获得该问题的解
  4. 该问题分解出的子问题是相互独立的

归并排序

归并排序就是分治法的典型应用之一,归并排序的实现有两种,一种是自顶向下的归并排序,另外一种就是自底向上的归并排序。排序

自顶向下的归并排序

向来看第一种,自顶向下的归并排序的实现思路是不断二分,而后对二分后的最小序列分别进行排序后,再将排序后的结果向上合并获得最终的有序数组,让咱们先经过一个树结构来理解归并排序的过程:
图片描述递归

从图中能够看到将一个数组0-14的元素不断二分,分到最后一层,而后互相比较,获得新的有序序列,而后向上合并,在进行比较,不断反复,合并出最终的有序序列,这就是自顶向下的归并排序的思路,经过这个思路你是否能本身写出排序的方法呢?索引

好了,接下来就让咱们看看具体的代码实现:

function sort(arr) {
  var n = arr.length
  if (n < 2) return arr
  var mid = Math.ceil(n / 2)
  var left = arr.slice(0, mid)
  var right = arr.slice(mid)
  return merge(sort(left), sort(right));
}

function merge(left, right){
  var result = []
  while (left.length && right.length) {
    if (left[0] < right[0]) {
      result.push(left.shift())
    } else {
      result.push(right.shift())
    }
  }
  while (left.length) {
    result.push(left.shift())
  }
  while (right.length) {
    result.push(right.shift())
  }
  return result;
}
var arr=[3,44,38,5,47,15,36,26,27,2,46,4,19,50,48];
console.log(sort(arr))

代码实现是否是很容易理解呢?相信你们通过仔细的思考事后都能看得懂,为了方便更好的理解,来看一下动图的排序过程:
图片描述

自底向上的归并排序

自底向上的归并排序思路是将长度为n的数组当作n个长度为1的数组,而后两两向上归并排序,获得新的数组,不断向上归并排序,直到获得长度为n的数组,这样的排序比以前自顶向下的排序代码要少,下面来看具体的代码实现:

function merge(arr, start, mid, end){
  var i = start, j= mid + 1, copy = []
  for (var k = start; k <= end; k++) {
    copy[k] = arr[k]
  }
  for (var k = start; k <= end; k++) {
    if (i > mid) { // 左边取完取右边的元素
      arr[k] = copy[j]
      j++
    } else if (j > end) { // 右边取完取左边的元素
      arr[k] = copy[i]
      i++
    } else if (copy[j] < copy[i]) { // 右边的元素小于左边的元素,取右边的元素
      arr[k] = copy[j]
      j++ 
    } else { // 左边的元素小于右边的元素,取左边的元素
      arr[k] = copy[i]
      i++
    }
  }
  console.log(arr)
}

function sort(arr) {
  var n = arr.length
  for (var i = 1; i < n; i = i + i) { // i子数组的大小
    for (var j = 0; j < n - i; j = j + i + i) { // j数组的索引
      var start = j
      var end = Math.min(j + i + i - 1, n-1)
      var mid = parseInt((start + end) / 2)
      merge(arr, start, mid, end)
    }
  }
}

var arr=[3,44,38,5,47,15,36,26,27,2,46,4,19,50,48];
sort(arr)

为了方便理解,已经在代码中加了必要的注释,可能这段代码比以前的要难理解一些,须要你们耐心多花一些时间去理解

快速排序

快速排序也是一种分治的排序算法,因为它实现简单而且效率比通常的排序算法高,所以,它的应用范围很是普遍,接下来让咱们来看快速排序的排序过程:

clipboard.png

将数组的第一个元素作为基准,从数组末尾开始找比基准小的数,找到就停下来,记下索引j,而后从基准右边开始找比基准大的数找到停下来,记下索引i,而后交换i和j上的元素,获得数组:

clipboard.png

而后继续从44的位置开始找比基准3小的一直移动到2,此时索引i和索引j相等,将基准3和i、j对应的值交换位置获得:

clipboard.png

此时基准数3前面的元素都是比它小的数,后面元素都是比它大的数,而后从基准数先后拆成两个数组,在递归执行前面一样的操做。
看来上面的排序过程,你是否是有代码的实现了呢?能够先试着写一下实现的代码,这样更容易理解,接下来就让我来看一下具体代码:

function sort(arr, left, right) {
  var temp = arr[left],i = left, j = right, t;
  if (left < right) {
    while (i != j) {
      while (arr[j] >= temp && i < j) {
        j--
      }
      while (arr[i] <= temp && i < j) {
        i++
      }
      if (i < j) {
        t = arr[i]
        arr[i] = arr[j]
        arr[j] = t
      }
    }
    arr[left] = arr[i]
    arr[i] = temp
    sort(arr, left, i - 1)
    sort(arr, i + 1, right)
  }
  return arr
}
var arr=[3,44,38,5,47,15,36,26,27,2,46,4,19,50,48];
console.log(sort(arr, 0, arr.length - 1))

看了代码以后是否是以为并不难呢?这种快速排序的实现其实有一个问题不知道你们注意到没有,当数组中有多个重复元素的时候,重复的元素只要排了一个就不须要重复排了,可是这中快速排序的实现并无考虑这种状况,即使重复的元素仍是会执行排序,所以有人提出了更优的快速排序方法“三向切分的快速排序”,让咱们先来看代码实现有什么不一样:

function sort(arr, left, right) {
  var temp = arr[left],i = left, j = right,k = left + 1, t;
  if (left < right) {
    while (k <= j) {
      if (arr[k] < temp) {
        t = arr[k]
        arr[k] = arr[i]
        arr[i] = t
        i++;
        k++;
      } else if (arr[k] > temp) {
        t = arr[k]
        arr[k] = arr[j]
        arr[j] = t
        j--;
      } else {
        k++;
      }
    }
    sort(arr, left, i - 1)
    sort(arr, j + 1, right)
  }
  return arr
}
var arr=[3,44,38,5,47,15,36,26,27,2,46,4,19,50,48,44,38];
console.log(sort(arr, 0, arr.length - 1))

整体思路和以前的同样保证基准值前面的比它小后面的比它大,惟一不一样的地方就是用了一个临时下标k去做比较,把小的丢到基准值前面,大的值就和末尾的值交换,获得新值再与基准值比较,当与基准值相等的时候,就只须要将临时下标的值+1而不须要排序了

总结

这篇文章详细介绍了希尔排序、归并排序、快速排序这三种排序的思想和实现方式,但愿你们看完以后都能本身去多实践,多思考,算法仍是须要本身多动手,不然光看做用不大。若是有错误或不严谨的地方,欢迎批评指正,若是喜欢,欢迎点赞收藏

相关文章
相关标签/搜索