常见排序算法
算法与数据结构是我们计算机开发与学习中重要的基础,是每个开发者都应熟练掌握的内功,本篇博文主要记录一下常见的排序算法以及优化策略。
O(n^2)时间复杂度的简单排序算法
选择排序
选择排序的思想很简单,遍历数组每次选出最大(最小)的值放置在合适的位置即可。
1 | // 交换数组两个位置的元素 |
对于选择排序而言,我们有一个简单的优化策略——每次遍历同时选出最大值的最小值。这个优化方案有一个需要注意的点,当我们每次遍历选出最大值和最小值后,需要进行两次交换,需要考虑修正的情况。(假设排序的最大值在当前最小值的位置,先交换了最小值,此时最大值的位置也交换了,需要处理~^~)
1 | /** |
冒泡排序
它重复地走访过要排序的元素列,依次比较两个相邻的元素,如果他们的顺序(如从大到小、首字母从A到Z)错误就把他们交换过来。走访元素的工作是重复地进行直到没有相邻元素需要交换,也就是说该元素已经排序完成 。
1 | /** |
对于此冒泡排序的实现,我们可以发现,每经过一次冒泡,最大值已经放置在了合适的位置,所以在内层的循环中可以不必考虑正确位置的比较。
1 | /** |
在查阅资料以及学习的过程中发现,其实冒泡排序还能进行更近一步的优化,当每次排序时,发现之后所有的元素都已经有序时,可以记录这个位置,下一次冒泡只考虑前面的元素即可。
1 | function bubbleSortBest (arr) { |
如果要求更高的话,我们可以发现外层for循环,其实还可以做一些小调整,循环次数可以减少。
插入排序
插入排序的基本思想是:每步将一个待排序的记录,按其关键码值的大小插入前面已经排序的文件中适当位置上,直到全部插入完为止。 (很类似大多数人玩扑克牌时,抓牌时的策略)
1 | /** |
根据插入排序的思想我们可以知道,插入排序前半部分的数组一定是有序的,所以想要快速找到待插入元素正确的插入位置,可以使用二分查找的思想。
1 | /** |
O(nlogn)时间复杂度的排序算法
归并排序
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
1 | /** |
对于以上递归实现的归并排序,有两个小点可以优化.
1 | function mergeSort (arr, left, right) { |
使用递归法实现一个算法时,优点是思路清楚,缺点是递归函数会频繁的调用自身,递归次数过多时可能会发生栈溢出的情况,所以在实际应用中,我们应该考虑使用迭代法,来实现归并排序。
1 | /** |
对于迭代法而言,同样也可以使用插入排序来优化。对于有序的两个子项,不进行merge操作
1 | function mergeSortBU (arr) { |
快速排序
快速排序的思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
1 | /** |
以上代码应该算是quickSort最简单的实现了,但是这个实现中有一个严重的缺陷,当我们的待排序数组已经是几乎有序的时候,我们直接选取第一个元素为标定点P的时候quickSort会退化为时间复杂度为O(n^2)级别的算法,所有我们可以随机选取标定点P可以解决这个问题。同时在小数组的时候使用插入排序,也是一种通用的优化策略。
1 | function _quickSort (arr, l, r) { |
除了排序数组近乎有序的情况下,当数组中存在大量重复值的时候,也可能出现算法时间复杂度退化为O(n^2)的情况。这时候我们可以使用二路快排的算法优化。二路快排的核心思想就是从数组两端开始扫描,即使遇到与标的点P相等的数据时,也进行交换位置。这样就使得quickSort在进行partition操作的时候将数组分为平衡的两个部分。
1 | function _quickSortTwoWay (arr, l, r) { |
基于二路快排的思想,我们可以发现其实我们重复的移动了与标定点P数值相等的元素,那么我们可以找到一种方法减少这种重复且无用的操作吗?答案当然是,这也就是quickSort最经典的优化三路快排。
1 | // 三路快排 |
未完待续
对于排序算法而言,以上列出的种种只是冰山一角,之后有兴趣的时候在一一完善记录吧~
(堆排序、希尔排序、计数排序、桶排序……)