目录:
另外这几天没事在刷算法题,感兴趣的可以看看:
前言
这是在掘金写的第一篇文章,花了几天重新修改了一下。
排序就是将一组对象按照某种逻辑顺序重新排列的过程。比如信用卡账单中的交易是按照日期排序的——这种排序很可能使用了某种排序算法。在计算时代早期,大家普遍认为30%的计算周期都用在了排序上,今天这个比例可能降低了,大概是因为现在的排序算法更加高效。现在这个时代数据可以说是无处不在,而整理数据的第一步往往就是进行排序。所有的计算机系统都实现了各种排序算法以供系统和用户使用。
即使你只是使用标准库中的排序函数,学习排序算法仍然有很大的实际意义:
- 排序算法往往是我们解决其他问题的第一步
- 排序算法有助于我们理解其他算法
- 算法在公司面试中占有很大比例,排序算法作为其中的重要组成部分,我们理所当然要学好了。
另外,更重的是下面介绍的这些算法都很经典,优雅而且高效,学习其中的精髓对自己提高自己的编程能力也有很大的帮助。 排序在商业数据处理和现代科学计算中有很重要的地位,它能够应用于事务处理,组合优化,天体物理学,分子动力学,语言学,基因组学,天气预报和很多其他领域。下面会介绍的一种排序算法(快速排序)甚至被誉为20世纪科学和工程领域的十大算法之一。后面我们会依次学习几种经典的排序算法,并高效地实现“优先队列”这种基础数据类型。我们将讨论比较排序算法的理论基础并中借若干排序算法和优先队列的应用。
初级排序算法
一 冒泡排序
图片来源:维基百科
1. 介绍:
冒泡排序(Bubble Sort),是一种计算机科学领域的较简单的排序算法。 它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越大的元素会经由交换慢慢“浮”到数列的顶端,故名“冒泡排序”。
2. 原理:
比较相邻的元素。如果第一个比第二个大,就交换他们两个。 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。针对所有的元素重复以上的步骤,除了最后一个。持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
3. 实现代码
public static void bubbleSort(int[] numbers) { int temp = 0; int size = numbers.length; for (int i = 0; i < size - 1; i++) { for (int j = 0; j < size - 1 - i; j++) { // 交换两数位置 if (numbers[j] > numbers[j + 1]) { temp = numbers[j]; numbers[j] = numbers[j + 1]; numbers[j + 1] = temp; } } } }复制代码
4. 简单优化
冒泡排序是最容易理解的一个排序算法。我们实现完后,发现其效率并不是很高。当序列已经有序的时候。会进行一些多余的比较。根据其两两比较的特性,可以推测出,如果一趟比较中连一次元素的交换操作都没发生,那么整个序列肯定是已经有序的了。据此给出优化版冒泡排序算法。
public void bubbleSort(int[] numbers) { // 初始化为无序状态 boolean sorted = false; int temp = 0; int size = numbers.length; for (int i = 0; i < size - 1&& !sorted; i++) { // 假设已经有序,若没有发生交换,则sorted维持为true,下次循环将直接退出。 sorted = true; for (int j = 0; j < size - 1 - i; j++) { if (numbers[j] > numbers[j + 1]) // 交换两数位置 { temp = numbers[j]; numbers[j] = numbers[j + 1]; numbers[j + 1] = temp; // 数组无序 sorted = false; } } } }复制代码
简单的对比图:
排序用到的数据:
int[] numbers = { 1314, 920, 360 , 20863,3456,246437,234 ,11,23,3232,2323,4343,2131,221,312,321,3,123,21,321,321,3,123,21,321,3,213,21,321,312,3,21,1314, 920, 360 , 20863,3456,246437,234 ,11,23,3232,2323,4343,2131,221,312,321,3,123,21,321,321,3,123,21,321,3,213,21,321,312,3,21,1314, 920, 360 , 20863,3456,246437,234 ,11,23,3232,2323,4343,2131,221,312,321,3,123,21,321,321,3,123,21,321,3,213,21,321,312,3,21,1314, 920, 360 , 20863,3456,246437,234 ,11,23,3232,2323,4343,2131,221,312,321,3,123,21,321,321,3,123,21,321,3,213,21,321,312,3,21};复制代码
优化之前:
备注:程序运行时间和你的机器也有很大关系,大多数情况下优化后的冒泡排序速度都更快一些。
二 选择排序
图片来源:维基百科
1. 介绍:
选择排序是另一个很容易理解和实现的简单排序算法。学习它之前首先要知道它的两个很鲜明的特点。1,运行时间和输入无关。为了找出最小的元素而扫描一遍数组并不能为下一遍扫描提供任何实质性帮助的信息。因此使用这种排序的我们会惊讶的发现,一个已经有序的数组或者数组内元素全部相等的数组和一个元素随机排列的数组所用的排序时间竟然一样长!而其他算法会更善于利用输入的初始状态,选择排序则不然。 2,数据移动是最少的。选择排序的交换次数和数组大小关系是线性关系。看下面的原理时可以很容易明白这一点。
2. 原理:
在要排序的一组数中,选出最小的一个数与第一个位置的数交换;然后在 剩下的数 当中再找最小的与第二个位置的数交换,如此循环到倒数第二个数和最后一个数比较为止。
3. 实现代码
public static void selectSort(int[] numbers) { // 数组长度 int size = numbers.length; // 中间变量 int temp = 0; for (int i = 0; i < size; i++) { // 待确定的位置 int k = i; // 选择出应该在第i个位置的数也就是选出剩下元素最小的那个 for (int j = size - 1; j > i; j--) { if (numbers[j] < numbers[k]) { k = j; } } // 交换两个数 temp = numbers[i]; numbers[i] = numbers[k]; numbers[k] = temp; } }复制代码
三 插入排序
图片来源:维基百科
1. 介绍:
通常人们整理桥牌的方法是一张一张的来,将每一张牌插入到其他已经有序牌中的适当位置。在计算机的实现中,为了给要插入的元素腾出空间,我们需要将其余所有元素在插入之前都向右移动以为。这种算法就叫,插入排序。 与选择排序一样,当前索引左边的所有元素都是有序的,但他们的最终位置还不确定为了给更小的元素腾出空间,他们可能会被移动。但是当索引到达数组的右端时,数组排序就完成了。和选择排序不同的是,插入排序所需的时间取决于输入中元素的初始顺序。也就是说对一个接近有序或有序的数组进行排序会比随机顺序或是逆序的数组进行排序要快的多。
2. 原理:
每步将一个待排序的记录,按其顺序码大小插入到前面已经排序的字序列的合适位置(从后向前找到合适位置后),直到全部插入排序完为止。
3. 实现代码
/** * 插入排序 * * 从第一个元素开始,该元素可以认为已经被排序 取出下一个元素,在已经排序的元素序列中从后向前扫描 * 如果该元素(已排序)大于新元素,将该元素移到下一位置 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置 将新元素插入到该位置中 重复步骤2 * * @param numbers * 待排序数组 */ public static void insertSort(int[] numbers) { int size = numbers.length; int temp = 0; int j = 0; for (int i = 0; i < size; i++) { temp = numbers[i]; // 假如temp比前面的值小,则将前面的值后移 for (j = i; j > 0 && temp < numbers[j - 1]; j--) { numbers[j] = numbers[j - 1]; } numbers[j] = temp; } }复制代码
四,希尔排序
图片来源:维基百科
1. 介绍:
这个排序咋一看名字感觉很高大上,这是以D.L.shell名字命名的排序算法。 为了展示初级排序算法性质的价值,我们来看一下基于插入排序的快速的排序算法——希尔排序。对于大规模乱序的数组插入排序很慢,因为它只会交换相邻的元素,因此元素只能一点一点地从数组的一端移动到另一端。如果最小的元素刚好在数组的尽头的话,那么要将它移动到正确的位置要N-1次移动。希尔排序为了加快速度简单地改进了插入排序,交换不相邻的元素以对数组的局部进行排序,并最终用插入排序将局部有序的数组排序。
2. 原理:
先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行依次直接插入排序。
3. 实现代码
/** * 希尔排序的原理:根据需求,如果你想要结果从大到小排列,它会首先将数组进行分组,然后将较大值移到前面,较小值 * 移到后面,最后将整个数组进行插入排序,这样比起一开始就用插入排序减少了数据交换和移动的次数,可以说希尔排序是加强版的插入排序 拿数组5, 2, * 8, 9, 1, 3,4来说,数组长度为7,当increment为3时,数组分为两个序列 * 5,2,8和9,1,3,4,第一次排序,9和5比较,1和2比较,3和8比较,4和比其下标值小increment的数组值相比较 * 此例子是按照从大到小排列,所以大的会排在前面,第一次排序后数组为9, 2, 8, 5, 1, 3,4 * 第一次后increment的值变为3/2=1,此时对数组进行插入排序, 实现数组从大到小排 */ public static void shellSort(int[] data) { int j = 0; int temp = 0; // 每次将长度缩短为原来的一半 for (int increment = data.length / 2; increment > 0; increment /= 2) { for (int i = increment; i < data.length; i++) { temp = data[i]; for (j = i; j >= increment; j -= increment) { // 如想从小到大排只需修改这里 if (temp > data[j - increment]) { data[j] = data[j - increment]; } else { break; } } data[j] = temp; } } }复制代码
排序算法进阶
五 归并排序
图片来源:维基百科
1, 介绍
归并即将两个有序的数组归并并成一个更大的有序数组。人们很快根据这个思路发明了一种简单的递归排序算法:归并排序。要将一个数组排序,可以先(递归地)将它分成两半分别排序,然后将结果归并起来。归并排序最吸引人的性质是它能保证任意长度为N的数组排序所需时间和NlogN成正比;它的主要缺点也显而易见就是它所需的额外空间和N成正比。简单的归并排序如下图:
2,原理
归并(Merge)排序法是将两个(或两个以上)有序表合并成一个新的有序表,即把待排序序列分为若干个子序列,每个子序列是有序的。然后再把有序子序列合并为整体有序序列。
合并方法:
设r[i…n]由两个有序子表r[i…m]和r[m+1…n]组成,两个子表长度分别为n-i +1、n-m。
1、j=m+1;k=i;i=i; //置两个子表的起始下标及辅助数组的起始下标2、若i>m 或j>n,转⑷ //其中一个子表已合并完,比较选取结束3、//选取r[i]和r[j]较小的存入辅助数组rf 如果r[i]
3,实现代码
/** * 归并排序 简介:将两个(或两个以上)有序表合并成一个新的有序表 * 即把待排序序列分为若干个子序列,每个子序列是有序的。然后再把有序子序列合并为整体有序序列 时间复杂度为O(nlogn) 稳定排序方式 * * @param nums * 待排序数组 * @return 输出有序数组 */ public static int[] mergeSort(int[] nums, int low, int high) { int mid = (low + high) / 2; if (low < high) { // 左边 mergeSort(nums, low, mid); // 右边 mergeSort(nums, mid + 1, high); // 左右归并 merge(nums, low, mid, high); } return nums; } /** * 将数组中low到high位置的数进行排序 * * @param nums * 待排序数组 * @param low * 待排的开始位置 * @param mid * 待排中间位置 * @param high * 待排结束位置 */ public static void merge(int[] nums, int low, int mid, int high) { int[] temp = new int[high - low + 1]; int i = low;// 左指针 int j = mid + 1;// 右指针 int k = 0; // 把较小的数先移到新数组中 while (i <= mid && j <= high) { if (nums[i] < nums[j]) { temp[k++] = nums[i++]; } else { temp[k++] = nums[j++]; } } // 把左边剩余的数移入数组 while (i <= mid) { temp[k++] = nums[i++]; } // 把右边边剩余的数移入数组 while (j <= high) { temp[k++] = nums[j++]; } // 把新数组中的数覆盖nums数组 for (int k2 = 0; k2 < temp.length; k2++) { nums[k2 + low] = temp[k2]; } }复制代码
六 快速排序
图片来源:维基百科
前言:
快速排序可能是应用最广泛的排序算法了。快速排序流行的原因主要因为它实现简单,适用于不同的输入数据且在一般应用中比其他排序算法都要快得多。快速排序引人注目的特点包括它是原地排序(只需要一个很小的辅助栈),且将长度为N的数组排序所需要的时间和NlgN成正比。我们之前提到的几种排序算法都无法将这两个优点结合起来。另外,快速排序的内循环比大多数排序算法都要短小,这意味着它无论是理论上还是实际中都要更快。它的主要缺点是非常脆弱,在实现时要非常小心才能避免低劣的性能。已经有无数例子显示许多错误都能致使它在实际应用中的性能只有平方级别。不过还好,我们由这些缺点和教训中大大改进了快速排序算法,使它的应用更加广泛。
1,介绍
快速排序是一种分治的排序算法。它将一个数组分成两个字数组,将两部分独立地排序。 快速排序和归并排序是互补的:归并排序将数组分成两个字数组分别排序,并将有序的字数组归并以将整个数组排序;而快速排序将数组排序的方式则是当两个字数组都有序时整个数组也就自然有序了。在第一种情况中,递归调用发生在处理整个数组之前;在第二种情况中,递归调用发生在处理整个数组之后。在归并排序中,一个数组被等分为两半;快速排序中,切分的位置取决于数组的内容。快速排序的过程大致如下:
2,原理
快速排序的基本思想:
通过一趟排序将待排序记录分割成独立的两部分,一部分全小于选取的参考值,另一部分全大于选取的参考值。这样分别对两部分排序之后顺序就可以排好了。 例子:
(a)一趟排序的过程:
3,实现代码
/** * 查找出中轴(默认是最低位low)的在numbers数组排序后所在位置 * * @param numbers 带查找数组 * @param low 开始位置 * @param high 结束位置 * @return 中轴所在位置 */ public static int getMiddle(int[] numbers, int low, int high) { // 数组的第一个作为中轴 int temp = numbers[low]; while (low < high) { while (low < high && numbers[high] > temp) { high--; } // 比中轴小的记录移到低端 numbers[low] = numbers[high]; while (low < high && numbers[low] < temp) { low++; } // 比中轴大的记录移到高端 numbers[high] = numbers[low]; } numbers[low] = temp; // 中轴记录到尾 return low; // 返回中轴的位置 } /** * * @param numbers 带排序数组 * @param low 开始位置 * @param high 结束位置 */ public static void quick(int[] numbers, int low, int high) { if (low < high) { int middle = getMiddle(numbers, low, high); // 将numbers数组进行一分为二 quick(numbers, low, middle - 1); // 对低字段表进行递归排序 quick(numbers, middle + 1, high); // 对高字段表进行递归排序 } } /** * 快速排序 * 快速排序提供方法调用 * @param numbers 带排序数组 */ public static void quickSort(int[] numbers) { // 查看数组是否为空 if (numbers.length > 0) { quick(numbers, 0, numbers.length - 1); } }复制代码
4,改进方法
我们只介绍一种常用的,具体代码就不贴出了。想去的可以去https://algs4.cs.princeton.edu/code/ 这个网站找。
三向切分快速排序 :
核心思想就是将待排序的数据分为三部分,左边都小于比较值,右边都大于比较值,中间的数和比较值相等.三向切分快速排序的特性就是遇到和比较值相同时,不进行数据交换, 这样对于有大量重复数据的排序时,三向切分快速排序算法就会优于普通快速排序算法,但由于它整体判断代码比普通快速排序多一点,所以对于常见的大量非重复数据,它并不能比普通快速排序多大多的优势 。
七 堆排序
介绍:
堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是是对简单选择排序的改进。。可以利用数组的特点快速定位指定索引的元素。堆分为大根堆和小根堆,是完全二叉树。大根堆的要求是每个节点的值都不大于其父节点的值,即A[PARENT[i]] >= A[i]。在数组的非降序排序中,需要使用的就是大根堆,因为根据大根堆的要求可知,最大的值一定在堆顶。
原理:
将待排序的序列构造成一个大顶堆。此时,整个序列的最大值就是堆顶的根节点。将它移走(其实就是将其与堆数组的末尾元素交换,此时末尾元素就是最大值),然后将剩余的n-1个序列重新构造成一个堆,这样就会得到n个元素中的次最大值。如此反复执行,就能得到一个有序序列了。
-
堆节点的访问
通常堆是通过一维数组来实现的。在数组起始位置为0的情形中:
- 父节点i的左子节点在位置2i+1;
- 父节点i的右子节点在位置 2i+2;
- 子节点i的父节点在位置floor((i-1/2)
-
堆的操作
在堆的数据结构中,堆中的最大值总是位于根节点(在优先队列中使用堆的话堆中的最小值位于根节点)。堆中定义以下几种操作:
- 最大堆调整(Max_Heapify):将堆的末端子节点作调整,使得子节点永远小于父节点
- 创建最大堆(Build_Max_Heap):将堆所有数据重新排序
- 堆排序(HeapSort):移除位在第一个数据的根节点,并做最大堆调整的递归运算
3, 实现代码
public class HeapSort { private int[] arr; public HeapSort(int[] arr){ this.arr = arr; } /** * 堆排序的主要入口方法,共两步。 */ public void sort(){ /* * 第一步:将数组堆化 * beginIndex = 第一个非叶子节点。 * 从第一个非叶子节点开始即可。无需从最后一个叶子节点开始。 * 叶子节点可以看作已符合堆要求的节点,根节点就是它自己且自己以下值为最大。 */ int len = arr.length - 1; int beginIndex = (len - 1) >> 1; for(int i = beginIndex; i >= 0; i--){ maxHeapify(i, len); } /* * 第二步:对堆化数据排序 * 每次都是移出最顶层的根节点A[0],与最尾部节点位置调换,同时遍历长度 - 1。 * 然后从新整理被换到根节点的末尾元素,使其符合堆的特性。 * 直至未排序的堆长度为 0。 */ for(int i = len; i > 0; i--){ swap(0, i); maxHeapify(0, i - 1); } } private void swap(int i,int j){ int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; } /** * 调整索引为 index 处的数据,使其符合堆的特性。 * * @param index 需要堆化处理的数据的索引 * @param len 未排序的堆(数组)的长度 */ private void maxHeapify(int index,int len){ int li = (index << 1) + 1; // 左子节点索引 int ri = li + 1; // 右子节点索引 int cMax = li; // 子节点值最大索引,默认左子节点。 if(li > len) return; // 左子节点索引超出计算范围,直接返回。 if(ri <= len && arr[ri] > arr[li]) // 先判断左右子节点,哪个较大。 cMax = ri; if(arr[cMax] > arr[index]){ swap(cMax, index); // 如果父节点被子节点调换, maxHeapify(cMax, len); // 则需要继续判断换下后的父节点是否符合堆的特性。 } } /** * 测试用例 * * 输出: * [0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, 5, 5, 5, 6, 6, 6, 7, 7, 7, 8, 8, 8, 9, 9, 9] */ public static void main(String[] args) { int[] arr = new int[]{ 3,5,3,0,8,6,1,5,8,6,2,4,9,4,7,0,1,8,9,7,3,1,2,5,9,7,4,0,2,6}; new HeapSort(arr).sort(); System.out.println(Arrays.toString(arr)); } }复制代码
几种排序算法的区别与排序算法的应用
稳定性:
如果一个排序算法能够保留数组中重复元素的相对位置则可以被称为是稳定的。这个性质在许多情况下很重要。例如:在考虑一个需要处理大量含有地理位置和时间戳的事件互联网商业应用程序。首先,我们在时间发生时将它们挨个存储在一个数组中,这样在数组中它们已经是按照时间顺序排序好了的。现在假设在进一步处理前将按照地理位置划分。一种简单的方法是将数组按照位置排序。如果排序算法不是稳定的,排序后的每个城市的交易可能不会再是按照时间顺序排序的了。很多情况下,不熟悉排序稳定性的程序员在第一次遇到这种情形的时候会在使用不稳定排序算法后把数组弄的一团糟而一脸懵逼。上一篇文章中我们讲到的插入排序和归并排序都属于稳定的,其他几个(选择,希尔,快速,堆排序)都是不稳定的。有很多办法能够将任意排序算法编程稳定的,但一般只有在稳定性是必要的情况下稳定的排序算法才有优势。不要想当然认为算法具有稳定性是理所当然的,但事实上没有任何实际应用中常见的算法不是用了大量额外的时间和空间才做到了这一点(研究人员开发了这样的算法,但应用程序员发现它们太复杂了,无法在实际开发中使用)。 上述例子如下图所示:
我们究竟该使用哪种算法
下表总结了我们面试常见的排序算法的各种重要性质。除了希尔排序(它的复杂度只是一个近似),插入排序(它的复杂度取决于输入元素的排列情况)和快速排序的两个版本(它门的复杂度和概率有关,取决于输入元素的分不清情况)之外,将这些运行时间的增长数量级乘以适当的常数就能够大致估计出其运行时间。这里的常数有时和算法有关(比如堆排序的比较次数是归并排序的两倍,且两者访问数组的次数都比快速排序多得多),但主要取决于算法的实现,Java编译器以及你的计算机,这些因素决定了需要执行的机器指令的数量以及每条指令所需的执行时间。最重要的是,因为这些都是常数,你能通过较小的N得到的实现数据和我们标准双倍测试来推测较大N所需的运行时间。
Java系统库的排序算法:
Java系统库主要排序方法java.util.Arrays.sort()。更具不同的参数类型,它实际代表了一系列排序方法:
- 每种原始数据类型都有一个不同排序方法;
- 一个适用于所有实现了Comparable接口的数据类型的排序方法;
- 一个适用于实现了比较器Comparator的数据类型的排序方法。 Java的系统程序员选择对原始数据类型使用(三向切分的)快速排序,对引用类型使用归并排序。这些选择实际上也暗示用速度和空间(对于原始数据类型)来换取稳定性(对于引用类型)。 当为实际应用开发Java程序,你会发现Java的Arrays.sort()发现(可能加上你自己实现的compareTo()或者compare())已经基本够用了,因为它使用的三向快速排序和归并排序都是经典。 下面看一个重写compareTo方法来实现按年龄排序的实例:
package map;import java.util.Set;import java.util.TreeMap;public class TreeMap2 { public static void main(String[] args) { // TODO Auto-generated method stub TreeMappdata = new TreeMap (); pdata.put(new Person("张三", 30), "zhangsan"); pdata.put(new Person("李四", 20), "lisi"); pdata.put(new Person("王五", 10), "wangwu"); pdata.put(new Person("小红", 5), "xiaohong"); //得到key的值的同时得到key所对应的值 Set keys = pdata.keySet(); for (Person key : keys) { System.out.println(key.getAge() + "-" + key.getName()); } }}// person对象没有实现Comparable接口,所以必须实现,这样才不会出错,才可以使treemap中的数据按顺序排列// 前面一个例子的String类已经默认实现了Comparable接口,详细可以查看String类的API文档,另外其他// 像Integer类等都已经实现了Comparable接口,所以不需要另外实现了class Person implements Comparable { private String name; private int age; public Person(String name, int age) { super(); this.name = name; this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } /** *TODO重写compareTo方法实现按年龄来排序 */ @Override public int compareTo(Person o) { // TODO Auto-generated method stub if (this.age > o.getAge()) { return 1; } else if (this.age < o.getAge()) { return -1; } return age; }}复制代码
参考:
《算法》
维基百科:https://zh.wikipedia.org/wiki/堆排序
欢迎关注我的微信公众号(坚持原创,分享美文,分享各种Java学习资源,面试题,以及企业级Java实战项目回复关键字免费领取):