排序算法汇总(java实现,附源代码)
整理系统的时候发现了原来写的各种算法的总结,看了一下,大吃一惊,那时候的我还如此用心,具体的算法,有的已经模糊甚至忘记了,看的时候就把内容整理出来,顺便在熟悉一下,以后需要的时候就可以直接过来摘抄了。下面是总结的几个常用的排序算法:
- 插入排序
- 快速排序
- 冒泡排序
- 堆排序
- 计数排序
- 桶排序
可能大家对插入排序,快速排序,冒泡排序比较常用,在满足需求的时候也简单一些,下面逐一说一下每个算法的实现方式,不保证是写的最有效率的,但是能保证的是,各种算法的中心思想是对的,大家领会精神即可:
插入排序:
插入排序在生活中最真实的场景就是在打牌的时候了,尤其在斗地主的时候使用的最频繁了,我们每天在不知不觉中都在使用这种算法,所以请不要告诉大家你不会算法,来我们大家一起重温那温馨的时刻,首先上家搬牌,作为比较聪明伶俐的人,手急眼快的人,我喜欢不看牌,叩成一摞,最后看,这跟排序算法没毛关系,略过,都抓完之后看牌。从左到右,看抓的什么牌,一看,第一圈抓了个5,第二圈来个2,假如说有强迫症,牌必须是按照顺序排好的,由于按照顺序排放,所以要满足,如果你左边的第一张牌的点数小于你抓牌的点数,那么左边所有牌的点数都小于所抓牌的点数,这时候你就要比较,5>2(只论点数),然后你手里的牌就是2---5,第三圈来个4,牌都好小,这时候你就拿4和5比(或者拿2比都一样),4<5,然后4应该在5的左边,现在的顺序暂时是2---4---5,因为还不确定4左边的牌的点数是不是小于4,所有要找到所在位置左边第一个小于4的点数,把4放在这个点数的右边,好巧2<4,所以现在的顺序确定为2---4---5,第四轮抓了一个3,都太小了,但是依然需要把3插到对应的位置,首先3<5,那么放到5的前面,顺序暂时是2---4---3---5,3继续向左边寻找,知道找到比它小的那张牌,然后它和4比较结果暂时为2---3---4---5,然后在跟2做对比,得出结果,顺序为2---3---4---5,以此类推,不知不觉中就完成了插入排序的算法。
过程就是这个样子,我们看一下在代码中如何实现:
1 public void sort(int[] data) {
2 for (int j = 1; j < data.length; j++) {
3 int i = j - 1;
4 int key = data[j];
5 while (i >= 0 && data[i] > key) {
6 data[i + 1] = data[i];
7 data[i] = key;
8 i -= 1;
9 }
10 }
11 }
由于我用数组来实现,下标从0开始,所以看起来可能觉得怪怪的,这让这段代码看起来可能有点不好理解,
第2-10行:的循环就是我们看牌的过程,为什么从第张开始,是因为看第一张的时候没有比较,姑且认为它(点数为5)是最小的,
第3行:的意思就是告诉我当前看的这张牌将要和那个位置的牌做比较(应该都是跟现在看的牌所在位置的前一个位置的牌做比较),我第二张抓到的是2,他就要跟第与第一张比较,
第4行:看这张牌是什么点数。
第5行:如果还没有比到头,切前一个位置牌的点数大于这张牌的点数,那么将他们俩换位置。
第6-7行:互换位置。
第8行:由于这张牌的位置前移了一位,那么下次做比较牌的位置也要向前移动一位。
OK,插入排序很简单就说道这里了。适合数据量小的情况下排序使用。
快速排序:
请宽恕我生活阅历不高,快速排序我确实是找不到合适生活中的应用场景了,就画个小图来说明一下吧:
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
1 | 6 | 8 | 4 | 1222 | 29 | 872 | 5 |
这些数据是用来排序的,快速排序是通过把将要排序的数组进行拆分,然后局部进行排序,在程序开始的首先要取数组中的左后一个位置保存的值作为所进行排序的数组“分水岭”,就是把比这个值小的数都放在这个数的左边,比这个数大的数都放到这个数的右边。这是如何实现的,无非是两方面,比较和位置调整。
需要将数组中的所有数据都进行比较,如果有需要,那么调整位置,调整位置分为主动调整和被动调整两种情况。
主动调整:
需要满足两个条件:(1)小于最后一个位置保存的数据值。(2)所在位置前面的位置中保存的数值大于最后一个位置保存的数值。
主动调整的时候要将数据保存最后一个已经比较过得知保存的数值小于最后一个位置保存的数据值位置的后一位,是不是特别拗口,那上面的例子为例上面表格中上面的数字是对应数组的下标,在第一次比较的时候,比较1<5,前面没有大于5的值,位置不用变,6>5,也不用变,8>5,也不用变,4<5,且在坐标为1的位置上保存的数值为6,所有4需要主动调整,此时最后一个已经比较过得知保存的数值小于最后一个位置保存的数据值的位置为0,要讲4保存在1的位置,但是1的位置保存的值是6,所有6就要做被动调整。
被动调整:
由于本身的位置即将被别人占用,所做的调整。被动调整位置就是和发起主动调整的数据进行位置互换,最后将发起主动调整的数值保存到空出来的坐标上,发生后移的数值可以肯定的认为是大于最后一个下标所保存的数值的,不然早就被主动调整了。如果还是不好理解那么还是以本例子为例:在比较到4的时候,需要调整,需要被动调整的下标起始位置为1,发起主动调整的数值的下标为3,那么就和小标3保存的值8进行位置互换,后移之后的结果为:
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
1 | 4 | 8 | 6 | 1222 | 29 | 872 | 5 |
第一轮比较之后,发现已经没有需要调整位置的时候了,这时候该调整最后一个下标保存值的位置了,因为前面说在先要取数组中的左后一个位置保存的值作为所进行排序的数组“分水岭”所以大家应该知道需要把下标为7保存的值5主动到下标下标为2的位置。然后进行位置调整。调整方法前面已经说过,不再重复。最终结果如下:
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
1 | 4 | 5 | 6 | 1222 | 29 | 872 | 8 |
接下来将要分配的数组分为两部分:
0 | 1 | 2 |
1 | 4 | 5 |
3 | 4 | 5 | 6 | 7 |
6 | 1222 | 29 | 872 | 8 |
对这两部分重新进行操作,所以快速排序,需要用递归的方式计算。下面就需要保存的几个变量做一下说明:
第一:是最后一个下标的值,因为在这个值在进行主动调整位置的时候,所在数组下标的位置会被覆盖。如上例中的下标 7.
第二:正在与最后值进行比较的数据的下标k,保存k是为了通过比较之后万一需要位置调整的时候,依次后移到的位置。
第三:已经与左后一个坐标保存的值比较过,明确不需要调整位置的最后一个数组下标i,保存这个变量是因为,如果后面的第k个下标元素需要调整位置,需要i+1下标上的数值和k下标的数值互换。
下面来看一下快速排序算法的代码实现:
1 public void sort(int[] data) {
2 quickSort(data, 0, data.length - 1);
3 }
4
5 public void quickSort(int[] data, int p, int r) {
6 if (p < r) {
7 int q = partition(data, p, r);
8 quickSort(data, p, q - 1);
9 quickSort(data, q + 1, r);
10 }
11 }
12
13 public int partition(int[] data, int p, int r) {
14 int x = data[r];
15 int i = p - 1;
16 for (int j = p; j <= r; j++) {
17 if (data[j] < x && i < j) {
18 i++;
19 int key = data[j];
20 data[j] = data[i];
21 data[i] = key;
22 // if (i != j) {
23 // data[i] += data[j];
24 // data[j] = data[i] - data[j];
25 // data[i] -= data[j];
26 // }
27 }
28 }
29 int key = x;
30 data[r] = data[i + 1];
31 data[i + 1] = key;
32
33 // if ((i + 1) != r) {
34 // data[i + 1] += data[r];
35 // data[r] = data[i + 1] - data[r];
36 // data[i + 1] -= data[r];
37 // }
38
39 return 1 + i;
40 }
如果要是能够将注释部分互换位置的部分闹明白,尤其是判断条件,那么就真的理解快速排序法了。例子中quickSort方法实现了递归调用,重要的方法是partition,它完成了拆分排序数组,和位置调整的操作。
在partition中,我们用x来保存要比较的值。用i来保存已经确定的不需要位置调整的下标,从-1开始,用j来保存正在比较验证的数值的数组下标,从0开始。
冒泡排序:
这个应该是最常见的,大概也就是在要排序的集合的最后面开始,用第一个元素依次和前面的比较,如果比前面的小,那么互换位置,然后在跟前一个比较,直到比较到一个位置(这个位置前面的数值一定小于正在比较的数值)。所以也需要用到两个变量,一个用来标记正在比较的数值的下标,一个用来保存那个“位置”,猜测大家都知道,所以就对于冒泡排序就不多废话了,上代码:
1 public void sort(int[] data) {
2 for (int i = 0; i < data.length; i++) {
3 for (int j = data.length - 1; j > i + 1; j--) {
4 if (data[j - 1] > data[j]) {
5 data[j - 1] = data[j - 1] + data[j];
6 data[j] = data[j - 1] - data[j];
7 data[j - 1] -= data[j];
8 }
9 }
10 }
11 }
很简单。
堆排序:
不得不说,堆排序我已经不记得了,看了一会才看明白,看明白之后,我就对发明堆派寻的人很崇拜,多么牛逼的算法。
总的来说堆排序就是把数组a[0.....n]中总找出一个最大值,然后把最大值max放到数组的最后a[n]=max,然后再从a[0.....n-1]中找出一个最大值,然后把最大值保存在a[0.....n-1]中,依次类推。堆排序的关键就是如何最快的找到一定范围内的最大值。
堆排序是通过最大堆的方式找到最大值,先了解一些概念:
二叉堆:就理解成二叉树就行了。
最大堆:满足所有的满足父>子。
最小堆:满足所有子>父。
首要要做的就是把要排序的数组构造成一个二叉堆,臆想的,比如排序的数组为:
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
1 | 6 | 8 | 4 | 1222 | 29 | 872 | 5 |
然后我们通过想象构造出了如下一个二叉堆,构造二叉堆的方法很简单,从头到位按照二叉树的方式构造,构造结果如下:
通过构造的二叉堆我们需要得出对我们有用的信息,有用的信息就是指每个元素在二叉堆中的孩子节点所在数组中的坐标,知道二叉树的人不用看二叉堆都知道节点i的子节点的下标应该是2*i+1,已经2(i+1),因为我们是从下标0开始计算。数字就是通过下标来形象的描述二叉堆。
二叉堆构造完成以后,那么我们来说一下是如何将二叉堆构造成最大堆,构造方法是,用a[i]与他的两个孩子a[2*i+1],a[2(i+1)](如果存在的话)比较,将a[i],a[2*i+1],a[2(i+1)]中最大的值与a[i]位置互换,那么i从哪开始算呢?很简单就是找到最后一个有孩子的节点,因为我们是从0开始计算,那么这个下标应该是从a.lenth-1/2到0,这样一轮下来a[0]就是最大值,代码实现:
1 package sort;
2
3 /**
4 * O(nlgn) 把数据假象构造成一个二叉堆,每个节点的左右坐标跟别为2i,2i+1. 构造Max树:
5 *
6 * @author Think
7 *
8 */
9 public class HeapSort implements ISort {
10 int dataLength = 0;
11
12 /***
13 * 将最大的值拿出来,放到最后,然后通过1--n-1个数据,找新的最大数
14 */
15 @Override
16 public void sort(int[] data) {
17 dataLength = data.length;
18 buildMaxHeap(data);
19 for (int i = dataLength - 1; i >= 1; i--) {
20 data[i] += data[0];
21 data[0] = data[i] - data[0];
22 data[i] -= data[0];
23 dataLength--;
24 max_heapify(data, 0);
25 }
26 }
27
28 /**
29 * 保证二叉堆的性质 A[i] >= A[left[i]] A[i] >= A[right[i]]
30 * 在构造二叉堆和每次从最后移除一个元素以后都要重新组织二叉堆的结构
31 *
32 * @param data
33 * @param i
34 */
35 public void max_heapify(int[] data, int i) {
36 int largest = 0;
37 int l = 2 * i + 1;
38 int r = 2 * (i + 1);
39 if (l < dataLength && data[l] > data[i]) {
40 largest = l;
41 } else {
42 largest = i;
43 }
44 if (r < dataLength && data[r] > data[largest]) {
45 largest = r;
46 }
47 if (largest != i) {
48 data[i] += data[largest];
49 data[largest] = data[i] - data[largest];
50 data[i] -= data[largest];
51 max_heapify(data, largest);
52 }
53 }
54
55 /**
56 * 构建最大堆
57 *
58 * @param data
59 */
60 public void buildMaxHeap(int[] data) {
61 for (int j = ((dataLength - 1) / 2); j >= 0; j--) {
62 max_heapify(data, j);
63 }
64 }
65 }
在sort方法中,做的内容是构造最大堆,把通过最大堆获取的最大值a[0]与a数组的最后一个下标保存的数值进行位置交换,然后在将最大值从构造最大堆的数据中排除。排除的方法是通过设置最大堆时候数组的界限,在例子中dataLength变量就是这个作用。
buildMaxHeap方法是第一次构造最大堆,大家可能有疑问在buildMaxHeap中调用max_heapify方法和在sort方法中调用max_heapify方法的第二个参数为何不一样,为什么有的从dataLength - 1开始递减,有的一直传0,有没有想过,其实不难回答,因为在排序开始的时候buildMaxHeap方法必须先构造出一个最大堆,此时对数据是没有任何假设的,数完全是随机的,我们不能保证a[0]保存的数值不是最大的,如果a[0]保存的数值是最大的,那么在和代码39-46行中largest变量永远的值都为0,没有结果,所以要从最下面开始逐一比较。但是在sort方法中执行max_heapify的时候对数据的排列就有了最大堆性质的保证。所以就可以从0开始,但是如果还是要从最后往前比较那也绝对是没有问题的。
由于交换完位置以后,可能导致被交换下去的较小的值,有小于它下面子节点值的可能,例如在本例子中的最后交换到1时候,刚交换完的时候应该是这样的:
所有为了确保最大堆的性质所以要递归排序,这段代码是通过47-51行来完成的。
计数排序:
我个人喜欢计数排序----简单,但是对于要排序的数据是有一些要求,比如要明确知道要排序的一组数的范围,输入的数据的值要在(0,k)的开区间内,以及数据最好要密集,如果这组数据的波动性较大不适合用技术排序的方式进行排序操作。计数排序的中心思想就是对于给定的数值key确定有多少数小于key,有多少值,那么这个值就是key值应该在排序后的数组中的下标。
方向找到以后就要找方法实现,计数排序的关键在于如何计算并且保存小于x值的数值的个数。技术排序是通过一个临时数组来保存,这个数组声明的长度就是我们前面所说的明确最大值的长度+1。具体如何保存的请看具体实现:
1 package sort;
2
3 /**
4 * 计数排序(前提条件,能够预先知道所需排序的上限,需要多余的一点空间) 适合数据密集。有明确范围的情况
5 *
6 * @author Think
7 *
8 */
9 public class CountSort {
10
11 public int[] sort(int[] data, int max_limit) {
12 int[] tmp = new int[max_limit];
13 int[] des = new int[data.length];
14 for (int i = 0; i < data.length; i++) {
15 tmp[data[i]] += 1;
16 }
17 for (int j = 1; j < max_limit; j++) {
18 tmp[j] += tmp[j - 1];
19 }
20 for (int k = data.length - 1; k >= 0; k--) {
21 des[tmp[data[k]] - 1] = data[k];
22 tmp[data[k]] -= 1;
23 }
24
25 return des;
26 }
27
28 }
12-13行,声明临时数组和最终需要生成的数组。14-16行是计数排序比较巧妙的地方,在tmp中第data[i]个坐标上设置累加1,在tmp数组中下标k的值,是在data数组中存在k值的个数。比如说在data数组中有3个5的数值,那么在tmp[5]中保存的值是3。比如需要排序的数组为:
5 | 4 | 6 | 3 | 2 | 1 |
那么tmp数组经过14-16行的循环操作以后的内容为:
0 | 1 | 1 | 1 | 1 | 1 | 1 |
我们根据tmp数组中的数据,以及tmp数组的下标就能通过17-19行的内容,算出要排序的数组每个数值在排序中应该排在什么位置,经过17-19行的处理,tmp数组中的内容为:
0 | 1 | 2 | 3 | 4 | 5 | 6 |
结合tmp的下标和数值可以知道,tmp数组的第(data[i])个下标保存的值减1,(tmp[data[i]]-1)就是data[i]数据经过排序后所在数组中的位置。
所以在20-23行进行赋值,排序完成。 其中22行中tmp[data[k]] -= 1这句话是用来处理在派寻的数组中有重复数值的情况,对重复值进行排序。
桶排序:
如果需要排序的数组中的数值分布均匀,而且在区间[0,1)内,那么桶排序是个不错的选择,桶排序就是把[0,1)区间的分割成10个大小相同的自区间,然后将数组中的数值恰当的“漏”到对应的区间中,比如0.12要漏到[0.1,0.2)的区间中,就是值k要满足区间的左边值<=k<区间的右边值。然后在通过插入排序或者快速排序等方式对每个区间的数值进行派寻,下面是我实现的桶派寻的代码,和书上说的有点不一样:
1 package sort;
2
3 public class BucketSort {
4
5 public double[] sort(double[] data) {
6 return bucket_sort(data);
7 }
8
9 public double[] bucket_sort(double[] data) {
10 double[] des = new double[data.length];
11 Bucket[] tmp = new Bucket[10];
12 for (int i = 0; i < tmp.length; i++) {
13 tmp[i] = new Bucket(0, null);
14 }
15 for (int i = 0; i < data.length; i++) {
16 Bucket bucket = new Bucket(data[i], null);
17 int bucket_list_index = (int) (data[i] * 10);
18 bucket_in_sort(tmp[bucket_list_index], bucket);
19 }
20 int j = 0;
21 for (int i = 0; i < tmp.length; i++) {
22 Bucket tmp_bucket = tmp[i].next;
23 while (tmp_bucket != null) {
24 des[j] = tmp_bucket.value;
25 tmp_bucket = tmp_bucket.next;
26 j++;
27 }
28 }
29 return des;
30 }
31
32 public void bucket_in_sort(Bucket sourct_bucket, Bucket bucket) {
33 Bucket tmp = sourct_bucket.next;
34 if (tmp == null) {
35 sourct_bucket.next = bucket;
36 return;
37 }
38 while (tmp.next != null) {
39 if (tmp.value > bucket.value) {
40 bucket.next = sourct_bucket.next;
41 sourct_bucket.next = bucket;
42 break;
43 }
44 tmp = tmp.next;
45 }
46 tmp.next = bucket;
47 }
48
49 public class Bucket {
50 double value;
51 public Bucket next;
52
53 public Bucket(double value, Bucket bucket) {
54 this.value = value;
55 this.next = bucket;
56 }
57
58 public double getValue() {
59 return value;
60 }
61
62 public void setValue(double value) {
63 this.value = value;
64 }
65
66 public Bucket getBucketList() {
67 return next;
68 }
69
70 public void setBucketList(Bucket next) {
71 this.next = next;
72 }
73
74 }
75
76 }
Bucket是我定义的一个对象,来帮助我实现桶排序,有value和next两个属性,value用来保存排序的数值,next表示桶中的下一个对象,如果不存在那么为空。第12-14行在每个桶中初始化一个链表对象Bucket,将来顺着这个链表连接“漏”在桶中的数值,第15-18行是给每个数值确定“漏”到那个桶中,然后在调用bucket_in_sort方法,在“漏”如桶中的过程中直接排序,将value值大的Bucket对象放到后面。第21-28行是逐个桶中去把数值拿出来,拿出来的数值就是排序完成的。
几个常见的排序算法就说完了,不同的算法解决不同场景下的问题,欢迎大家和我进行交流。
下面是源代码的下载地址: