聊一聊排序算法

聊一聊排序算法

原创 2016-08-11 Barret李靖

两月前花了些时间,将大学里学过的排序算法都复习了一遍,代码放在 github 上,没有整理。今天翻了翻代码,重新 review 了一遍,也顺便做了点记录。

下面花了不少篇幅,将基础排序、希尔、归并、快排、堆排序等都介绍了一通,懒得思考的同学可以略过代码直接看文字,文章对排序的基本思路都做了介绍。

本文的代码可以在这里找到:https://github.com/barretlee/algorithms

三种基本排序

插入排序和选择排序是两种最基本的排序算法,思路完全不一样,但是细思一番,还是挺有意思的:

insertion sort插入排序,思路简单来说就是把自己插入到已排好序的列表中去,交换也是颇为频繁的。

function insertion(input) {  for(var i = 1, len = input.length; i < len; i++) {    for(var j = i; j > 0; j--) {      if(input[j] < input[j - 1]) {        input[j] = [input[j - 1], input[j - 1] = input[j]][0];      }    }  }  return input;}

selection sort选择排序,选择排序只对最后一个被选中的元素排序。它会往后找到包括自己在内最小的一个元素,替换自己。简单来说就是把第 i 小的元素放到第 i 个序位上。

function selection(input) {  for(var i = 0, len = input.length; i < len - 1; i++) {    var min = i;    for(var j = i + 1; j < len; j++) {      if(input[j] < input[min]) {        min = j;      }    }    input[i] = [input[min], input[min] = input[i]][0];  }  return input;}

冒泡排序,就更简单了,从第一个元素开始,往后比较,遇到比自己小的元素就交换位置,交换的次数最多,自然也是性能最差的。

function bubble(input) {  for(var i = 0, len = input.length; i < len - 1; i++) {    for(var j = i + 1; j < len; j++) {      if(input[j] < input[i]) {        input[j] = [input[i], input[i] = input[j]][0];      }    }  }  return input;}

针对随机性排列不同(比如完全随机,顺序,倒序,半顺序等状态)的数据,三种效果也是不一样的,可以思考下。

希尔排序

上面提到了三种最基本的排序算法,这里要提到的希尔排序,有点不好理解。

代码:/chapters/chapter-2-sorting/2.1-elementary-sorts/shell.js

function shell(input) {  var h = 1;  var len = input.length;  while(h < Math.floor(len / 3)) {    h = h * 3 + 1;  }  while(h >= 1) {    for(var i = h; i < len; i++)  {      for(var j = i; j >= h; j -= h) {        if(input[j] < input[j - h]) {          input[j] = [input[j - h], input[j - h] = input[j]][0];        }      }    }    h = Math.floor(h / 3);  }  return input;}

算法复杂不代表需要很多的代码去实现,因为代码表达的是过程,通过循环等方式可以很迅速实现一个过程,而算法是处理问题的方法,把它表达清楚可能就得费不少唇舌,甚至还得配上一些图辅助阅读。

希尔排序,大概的思路就是不断地从整体上调整数据的顺序,将比较大的数据尽量往后挪,比较小的数据尽量往前挪。数据的搬移也不是一步完成,每一次搬移都会将数据分块,分块的目的是尽可能的搬移距离比较远的数据,从而减少比较操作和交换操作。

归并排序

基本排序和希尔排序是都是从头到尾去遍历数据,不可避免的带来很多交换操作。归并排序是一种用空间换时间的排序算法,一个数组截断成两个子数组,子数据排好序后合并到一起。

代码:/chapters/chapter-2-sorting/2.2-mergesort/merge.js

function merge(input1, input2) {  var i = 0, j = 0;  var output = [];  while(i < input1.length || j < input2.length) {    if(i == input1.length) {      output.push(input2[j++]);      continue;    }    if(j == input2.length) {      output.push(input1[i++]);      continue;    }    if(input1[i] < input2[j]) {      output.push(input1[i++]);    } else {      output.push(input2[j++]);    }  }  return output;}

上面是一个简单的合并算法,将两个有序数据合并为一个。有人应该会想到,既然一个数组可以打散成两个进行排序,那被打算的子数组是不是也可以继续被打散呢?

答案是肯定的。这是一种典型的分治思想,递归归并。

代码:/chapters/chapter-2-sorting/2.2-mergesort/mergeRecursiveTop2Bottom.js

function mergeRecursiveTop2Bottom(input) {

  return sort(input, 0, input.length - 1);

  function sort(arr, start, end) {    if(start >= end) {      return;    }    var mid = ((end - start) >> 1) + start;    sort(arr, start, mid);    sort(arr, mid + 1, end);    return merge(arr, start, mid, end);  }

  function merge(arr, start, mid, end) {    var i = start, j = mid + 1, tmp = [];    for(var k = start; k <= end; k++) {      tmp[k] = arr[k];    }    for(k = start; k <= end; k++) {      if(i > mid) {        arr[k] = tmp[j++];        continue;      }      if(j > end) {        arr[k] = tmp[i++];        continue;      }      if(tmp[i] < tmp[j]) {        arr[k] = tmp[i++];      } else {        arr[k] = tmp[j++];      }    }    return arr;  }}

上面的算法是自顶向下的递归归并,简单来说就是解决很多小问题,那么大问题也就自然而然的解决了;还有一种自底向上的归并,这种归并简单来说,就是把一个大问题分解为多个小问题,多个小问题的答案就能得出大问题的答案。从解决问题的方式来看,两种处理方式是互逆的。

代码:/chapters/chapter-2-sorting/2.2-mergesort/mergeRecursiveTop2Bottom.js

function sort(arr) {  for(var sz = 1, len = arr.length; sz < len; sz = sz * 2) {    for(var start = 0; start < len - sz; start += sz * 2) {      arr = merge(arr, start, start + sz - 1, Math.min(start + sz * 2 - 1, len - 1));    }  }  return arr;}// merge 函数同上

不过自底向上的归并,在代码上稍微难理解一些,脑海重要有清晰的画卷,知道程序跑到哪一步了,尤其还需要处理边界问题。

快排

上面讨论了归并排序,将一个数组拆分成两个,然后合并处理,进而有了递归归并的思考。

而本节提出了一种更加高效的排序方法,这种算法跟归并排序是互补的,归并排序大致思路是分-排序合,而本节提出的快排采用的思路是排序分-合,把排序这种损耗比较大的操作前置了,所以效率更高。

代码:/chapters/chapter-2-sorting/2.3-quicksort/quicksort.js

function quicksort(input) {  sort(0, input.length - 1);  return input;

  function sort(start, end) {    if(start >= end) {      return;    }    var mid = partition(start, end);    sort(start, mid - 1);    sort(mid + 1, end);  }

  function partition(start, end) {    var i = start, j = end + 1, k = input[start];    while(true) {      while(input[++i] < k) if( i === end) break;      while(input[--j] > k) if( j === start) break;      if(i >= j) break;      input[i] = [input[j], input[j] = input[i]][0];    }    input[j] = [input[start], input[start] = input[j]][0];    return j;  }}

这个算法写起来,感觉相当酸爽,因为这个排序思路太棒,情不自禁地热血沸腾。事实上,这个算法也是存在几个疑点的:

  • 代码中的 mid 这个「哨兵」为啥要取第一个呢?
  • partition 函数当 end - start 很小的时候效率还高么?

于是有了两个想法:

  • 使用 input 的中位数作为「哨兵」
  • 当 end - start 比较小的时候,大约为 5~15,改为其他比较高效的算法

今天只对第二个想法做了实践,基本改造如下:

代码:chapters/chapter-2-sorting/2.3-quicksort/quicksortImprove.js

var delta = 5;function quicksortImprove(input) {  sort(0, input.length - 1);  return input;

  // sort 和 partition 函数同上

  function insertion(start, end) {    for(var i = start + 1, len = end - start; i < end; i++) {      for(var j = i; j > start; j--) {        if(input[j] < input[j - 1]) {          input[j] = [input[j - 1], input[j - 1] = input[j]][0];        }      }    }  }}

优化后的快排

上面提到了快排和快排的改进算法。当待排序的数据中存在大量重复元素时,快排的效率会不太高,当遇到重复元素的时候,比较和交换都是赘余的,重复元素越多,性能越差,为了解决这个问题,我们引入了第三个变量,来标识重复元素区间,如下图所示:

+---------------------------------+|  <v  |  =v  |=========|   > v   |+---------------------------------+       ↑      ↑         ↑      lt      i         gt

大致的原理是:每次排序分组的时候,就会过滤掉重复元素,这样,进入递归的元素就少了很多,因此而提高效率。

代码:/chapters/chapter-2-sorting/2.3-quicksort/quick3way.js

function quick3way(input) {  sort(0, input.length - 1);  return input;

  function sort(start, end) {    if(start >= end) return;

    var lt = start, gt = end, i = start + 1, v = input[start];    while(i <= gt) {      if(input[i] < v) {        input[lt] = [input[i], input[i] = input[lt]][0];        lt++; i++;      } else if(input[i] > v) {        input[gt] = [input[i], input[i] = input[gt]][0];        gt--;      } else {        i++;      }    }    sort(start, lt - 1);    sort(gt + 1, end);  }}

优先队列,堆排序

从最开始基本的冒泡、插入、选择和希尔排序,到分治思想的延伸——归并排序(自顶向下和自底向上),再到归并排序的互补算法——快排,然后学习了新的数据结构——二叉堆,于是有了堆排序。

二叉堆是一种数据结构,他的每一个二叉树点元素数值都会比下面两个节点元素的数值要大,因为这种数据接口包含的信息量很大,而得到这种数据结构的成本是很低的,构建一个二叉堆的算法并不复杂:

代码:/chapters/chapter-2-sorting/2.4-priority-queues/priorityQueueAdd.js

function priorityQueueAdd(input) {  var output = [];

  output[1] = input[0];  for(var i = 1, len = input.length; i < len; i++) {    output = swim(output, input[i]);  }

  return output;

  function swim(arr, val) {    arr.push(val);    var k = arr.length - 1;    while(k > 1 && arr[k >> 1] < arr[k]) {      var p = k >> 1;      arr[p] = [arr[k], arr[k] = arr[p]][0];      k = p;    }    return arr;  }}

通过上浮的方式,不断插入新元素,既可形成一个二叉堆。这种优先队列最大的特点是,能够拿到很快拿到最大元素(顶部),当这个最大元素被删除(优先级最高的事务被处理完成)时,还能快速高效地将剩下的元素重整为一个二叉堆:

代码:/chapters/chapter-2-sorting/2.4-priority-queues/priorityQueueDelete.js

function priorityQueueDelete(input) {  var output = [];

  input.splice(1, 1);  output = sink(input);

  return output;

  function sink(arr) {    arr.splice(1, 0, arr.pop());    var k = 1, N = arr.length - 1;    while(2 * k <= N) {      var j = 2 * k;      if(j < N && arr[j] < arr[j + 1]) j++;      if(arr[k] >= arr[j]) break;      arr[k] = [arr[j], arr[j] = arr[k]][0];      k = j;    }    return arr;  }}

一个二叉堆能够快速拿到最大元素,并且能够立即重新调整为二叉堆,基于这个特性,就有了堆排序

代码:/chapters/chapter-2-sorting/2.4-priority-queues/heapSort.js

function heapSort(input) {  return sort(input);

  function sort (arr){    var N = arr.length - 1;    for(var k = N >> 2; k >= 1; k--) {      arr = sink(arr, k, N);    }    while(N > 1) {      arr[1] = [arr[N], arr[N] = arr[1]][0];      N--;      arr = sink(arr, 1, N);    }    return arr;  }  function sink(arr, k, N) {    while(2 * k <= N) {      var j = 2 * k;      if(j < N && arr[j] < arr[j + 1]) j++;      if(arr[k] >= arr[j]) break;      arr[k] = [arr[j], arr[j] = arr[k]][0];      k = j;    }    return arr;  }}

光看代码还是挺难理解的,脑海中必须有一个数组储存的堆模型。for 循环构造了堆(从 N/2 开始,跳过了所有大小为 1 的堆),注意,这里构造的并不是二叉堆,然后 while 循环将最大的元素 a[1] 和 a[n] 交换位置并修复堆,如此循环直到堆为空。

上面的排序用到的是 sink 方法,而 swim 方法也是可以用于排序算法之中的,这就是对应的下沉排序,感觉有点难理解。

小结

能够从上往下看到这里的,需要给你点个赞。算法的学习刚开始有点枯燥,也有点艰难,学着学着,慢慢的就能够领悟其中的趣味。

后续我也会投入一部分精力深入研究算法,希望可以通过一定量的算法实践大幅度提升自己的思维能力和动手能力。

时间: 2024-07-30 23:07:32

聊一聊排序算法的相关文章

经典排序算法 - 冒泡排序Bubble sort

 原文出自于 http://www.cnblogs.com/kkun/archive/2011/11/23/bubble_sort.html 经典排序算法 - 冒泡排序Bubble sort 原理是临近的数字两两进行比较,按照从小到大或者从大到小的顺序进行交换, 这样一趟过去后,最大或最小的数字被交换到了最后一位, 然后再从头开始进行两两比较交换,直到倒数第二位时结束,其余类似看例子 例子为从小到大排序, 原始待排序数组| 6 | 2 | 4 | 1 | 5 | 9 | 第一趟排序(外循环) 第

排序算法比较及其应用

一.将各种数据排序 只要实现了Comparable接口的数据类型就可以被排序. 但要使算法能够灵活地用不同字段进行排序,则是后续需要考虑的问题. 1.指针排序 在Java中,指针操作是隐式的,排序算法操作的总是数据引用,而不是数据本身. 2.键不可变 如果在排序后,用例还可以改变键值,那么数组很可能就不是有序的了.类似,优先队列也会乱套. Java中,可以用不可变数据类型作为键来避免这个问题,如String,Integer,Double和File都是不可变的. 3.廉价交换 使用引用的另一个好处

选择排序 —— 排序算法系列

假设我们有如下一个数组: 使用选择排序算法对这个数组进行排序,步骤如下: 第 1 次 在下标0到6之间找到最小的数字,我们可以发现最小的数字是15,它在下标为4的位置上: 把下标4上面的数字跟下标0上面的数字互换,得到排序如下图的数组: 第 2 次 在下标1到6之间找到最小的数字,我们可以发现最小的数字是33,它在下标为5的位置上: 把下标5上面的数字跟下标1上面的数字互换,得到排序如下图的数组: 第 3 次 在下标2到6之间找到最小的数字,我们可以发现最小的数字是48,它在下标为5的位置上:

排序算法Java版,以及各自的复杂度,以及由堆排序产生的top K问题

常用的排序算法包括: 冒泡排序:每次在无序队列里将相邻两个数依次进行比较,将小数调换到前面, 逐次比较,直至将最大的数移到最后.最将剩下的N-1个数继续比较,将次大数移至倒数第二.依此规律,直至比较结束.时间复杂度:O(n^2) 选择排序:每次在无序队列中"选择"出最大值,放到有序队列的最后,并从无序队列中去除该值(具体实现略有区别).时间复杂度:O(n^2) 直接插入排序:始终定义第一个元素为有序的,将元素逐个插入到有序排列之中,其特点是要不断的 移动数据,空出一个适当的位置,把待插

排序算法总结

各种排序算法总结  排序算法  插入排序 冒泡排序  选择排序  归并排序  快速排序 堆排序  计数排序  基数排序  桶排序  思想  构建有序序列 两两交换 每次找一个最小值 分治法思想 分治法思想 最小堆.最大堆 数字本身的属性  对数据选择多种基数  函数的映射关系.Hash  数据结构  数组  数组  数组  数组 不定   数组 数组 数组  数组  最差时间复杂度 O(n^2)   O(n^2)   O(n^2)   O(n*lgn)  O(n^2).改进O(n*lgn)  O

七大常见排序算法总结

文档版本 开发工具 测试平台 工程名字 日期 作者 备注 V1.0 2016.04.06 lutianfei none V1.1 2016.07.16 lutianfei 增加了归并排序说明 V2.0 2016.07.19 lutianfei 完善了排序算法的总结 排序另一种分法 外排序:需要在内外存之间多次交换数据才能进行 内排序: 插入类排序 直接插入排序 希尔排序 选择类排序 简单选择排序 堆排序 交换类排序 冒泡排序 快速排序 归并类排序 归并排序 排序方法 平均情况 最好情况 最坏情况

数据结构——各排序算法的比较

1.从时间复杂度比较  从平均时间复杂度来考虑,直接插入排序.冒泡排序.直接选择排序是三种简单的排序方法,时间复杂度都为O(n2),而快速排序.堆排序.二路归并排序的时间复杂度都为O(nlog2n),希尔排序的复杂度介于这两者之间.若从最好的时间复杂度考虑,则直接插入排序和冒泡排序的时间复杂度最好,为O(n),其它的最好情形同平均情形相同.若从最坏的时间复杂度考虑,则快速排序的为O(n2),直接插入排序.冒泡排序.希尔排序同平均情形相同,但系数大约增加一倍,所以运行速度将降低一半,最坏情形对直接

八种排序算法

最近一段时间自己在研究各种排序算法,于是自己写了一个八种排序算法的集合: /************************************************************************* > Copyright (c)2014 stay hungry,stay foolish !!! > File Name: sort.cpp > Author: kanty > Mail: [email protected] > Created Time:

排序算法 之 快速排序

快速排序是基于分治思想的一种排序算法,就像该方法的名字一样,速度比较快,所以叫做快速排序:它的平均时间复杂度为O(N*logN),最坏时间复杂度为O(n2),由于快速排序在序列元素数量多的时候速度比较快,所以很多语言内置的排序方法也是用快速排序实现的.快速排序也有很多优化的版本,比如在排序时基数的选择等等-下面就说一下一般的快速排序的实现. 基本思想: 快速排序的基本思想就是,先从待排序的序列中任选一个元素作为基数,然后将序列中的其他小于基数的元素放在基数的左边,大于或等于基数的元素放在基数的右