一、冒泡排序(Bubble Sort)
【原理】
比较两个相邻的元素,将值大的元素交换至右端。
【思路】
依次比较相邻的两个数,将小数放在前面,大数放在后面。即在第一趟:首先比较第1个和第2个数,将小数放前,大数放后。然后比较第2个数和第3个数,将小数放前,大数放后,如此继续,直至比较最后两个数,将小数放前,大数放后。重复第一趟步骤,直至全部排序完成。
第一趟比较完成后,最后一个数一定是数组中最大的一个数,所以第二趟比较的时候最后一个数不参与比较;
第二趟比较完成后,倒数第二个数也一定是数组中第二大的数,所以第三趟比较的时候最后两个数不参与比较;
依次类推,每一趟比较次数-1;
……
【举例】——要排序数组:int【】 arr={6,3,8,2,9,1};
第一趟排序:
第一次排序:6和3比较,6大于3,交换位置: 3 6 8 2 9 1
第二次排序:6和8比较,6小于8,不交换位置:3 6 8 2 9 1
第三次排序:8和2比较,8大于2,交换位置: 3 6 2 8 9 1
第四次排序:8和9比较,8小于9,不交换位置:3 6 2 8 9 1
第五次排序:9和1比较:9大于1,交换位置: 3 6 2 8 1 9
第一趟总共进行了5次比较, 排序结果: 3 6 2 8 1 9
---------------------------------------------------------------------
第二趟排序:
第一次排序:3和6比较,3小于6,不交换位置:3 6 2 8 1 9
第二次排序:6和2比较,6大于2,交换位置: 3 2 6 8 1 9
第三次排序:6和8比较,6大于8,不交换位置:3 2 6 8 1 9
第四次排序:8和1比较,8大于1,交换位置: 3 2 6 1 8 9
第二趟总共进行了4次比较, 排序结果: 3 2 6 1 8 9
---------------------------------------------------------------------
第三趟排序:
第一次排序:3和2比较,3大于2,交换位置: 2 3 6 1 8 9
第二次排序:3和6比较,3小于6,不交换位置:2 3 6 1 8 9
第三次排序:6和1比较,6大于1,交换位置: 2 3 1 6 8 9
第二趟总共进行了3次比较, 排序结果: 2 3 1 6 8 9
---------------------------------------------------------------------
第四趟排序:
第一次排序:2和3比较,2小于3,不交换位置:2 3 1 6 8 9
第二次排序:3和1比较,3大于1,交换位置: 2 1 3 6 8 9
第二趟总共进行了2次比较, 排序结果: 2 1 3 6 8 9
---------------------------------------------------------------------
第五趟排序:
第一次排序:2和1比较,2大于1,交换位置: 1 2 3 6 8 9
第二趟总共进行了1次比较, 排序结果: 1 2 3 6 8 9
---------------------------------------------------------------------
最终结果:1 2 3 6 8 9
---------------------------------------------------------------------
由此可见:N个数字要排序完成,总共进行N-1趟排序,每i趟的排序次数为(N-i)次,所以可以用双重循环语句,外层控制循环多少趟,内层控制每一趟的循环次数,即
for(int i=1;i
for(int j=1;j
//交换位置
}
冒泡排序的优点:每进行一趟排序,就会少比较一次,因为每进行一趟排序都会找出一个较大值。如上例:第一趟比较之后,排在最后的一个数一定是最大的一个数,第二趟排序的时候,只需要比较除了最后一个数以外的其他的数,同样也能找出一个最大的数排在参与第二趟比较的数后面,第三趟比较的时候,只需要比较除了最后两个数以外的其他的数,以此类推……也就是说,每进行一趟比较,每一趟少比较一次,一定程度上减少了算法的量。
用时间复杂度来说:
1.如果我们的数据正序,只需要走一趟即可完成排序。所需的比较次数C和记录移动次数M均达到最小值,即:Cmin=n-1;Mmin=0;所以,冒泡排序最好的时间复杂度为O(n)。
2.如果很不幸我们的数据是反序的,则需要进行n-1趟排序。每趟排序要进行n-i次比较(1≤i≤n-1),且每次比较都必须移动记录三次来达到交换记录位置。在这种情况下,比较和移动次数均达到最大值:
冒泡排序的最坏时间复杂度为:O(n2) 。
综上所述:冒泡排序总的平均时间复杂度为:O(n2) 。
【代码实现】
public class BubbleSort {
public static void main(String【】 args) {
int【】 arr = {6, 3, 8, 2, 9, 1};
System.out.println("排序前数组:");
for (int num : arr) {
System.out.println(num + " ");
}
for (int i = 0; i < arr.length - 1; i++) {//外层循环控制排序趟数
for (int j = 0; j < arr.length - 1 - i; j++) {//内层循环控制每一趟排序多少次
if (arr【j】 > arr【j + 1】) {
swap(arr, j, j + 1);
}
}
}
System.out.println("------------");
System.out.println("排序后数组:");
for (int num : arr) {
System.out.println(num + " ");
}
}
public static void swap(int【】 arr, int i, int j) {
int tmp = arr【i】;
arr【i】 = arr【j】;
arr【j】 = tmp;
}
}
二、选择排序(SelectionSort)
【原理】
每一趟从待排序的记录中选出最小的元素,顺序放在已排好序的序列最后,直到全部记录排序完毕。也就是:每一趟在n-i+1(i=1,2,…n-1)个记录中选取关键字最小的记录作为有序序列中第i个记录。
【基本思想】(简单选择排序)
给定数组:int【】 arr={里面n个数据};第1趟排序,在待排序数据arr【1】~arr【n】中选出最小的数据,将它与arrr【1】交换;第2趟,在待排序数据arr【2】~arr【n】中选出最小的数据,将它与arr【2】交换;以此类推,第i趟在待排序数据arr【i】~arr【n】中选出最小的数据,将它与arr【i】交换,直到全部排序完成。
【举例】——数组 int【】 arr={5,2,8,4,9,1};
第一趟排序:
最小数据1,把1放在首位,也就是1和5互换位置,
排序结果:1 2 8 4 9 5
-------------------------------------------------------
第二趟排序:
第1以外的数据{2 8 4 9 5}进行比较,2最小,
排序结果:1 2 8 4 9 5
-------------------------------------------------------
第三趟排序:
除1、2以外的数据{8 4 9 5}进行比较,4最小,8和4交换
排序结果:1 2 4 8 9 5
-------------------------------------------------------
第四趟排序:
除第1、2、4以外的其他数据{8 9 5}进行比较,5最小,8和5交换
排序结果:1 2 4 5 9 8
-------------------------------------------------------
第五趟排序:
除第1、2、4、5以外的其他数据{9 8}进行比较,8最小,8和9交换
排序结果:1 2 4 5 8 9
-------------------------------------------------------
注:每一趟排序获得最小数的方法:for循环进行比较,定义一个第三个变量temp,首先前两个数比较,把较小的数放在temp中,然后用temp再去跟剩下的数据比较,如果出现比temp小的数据,就用它代替temp中原有的数据。
【代码实现】
public class SelectionSort {
public static void main(String【】 args) {
int【】 arr = {5, 2, 8, 4, 9, 1};
System.out.println("交换之前:");
for (int num : arr) {
System.out.print(num + " ");
}
// 做第i趟排序
for (int i = 0; i < arr.length - 1; i++) {
int minIndex = i;
// 选最小的记录
for (int j = i + 1; j < arr.length; j++) {
//记下目前找到的最小值所在的位置
minIndex = arr【j】 < arr【minIndex】 ? j : minIndex;
}
swap(arr, i, minIndex);
}
System.out.println();
System.out.println("交换后:");
for (int num : arr) {
System.out.print(num + " ");
}
}
public static void swap(int【】 arr, int i, int j) {
int tmp = arr【i】;
arr【i】 = arr【j】;
arr【j】 = tmp;
}
}
选择排序的时间复杂度:简单选择排序的比较次数与序列的初始排序无关。 假设待排序的序列有n个元素,则比较次数永远都是n (n - 1) / 2。而移动次数与序列的初始排序有关。当序列正序时,移动次数最少,为 0。当序列反序时,移动次数最多,为3n (n - 1) / 2。
所以,综上,简单排序的时间复杂度为 O(n2)。
三、插入排序(Insertion sort)
插入排序对于少量元素的排序是很高效的,而且这个排序的手法在每个人生活中也是有的哦。你可能没有意识到,当你打牌的时候,就是用的插入排序。
【概念】
从桌上的牌堆摸牌,牌堆内是杂乱无序的,但是我们摸上牌的时候,却会边摸边排序,借用一张算法导论的图。
每次我们从牌堆摸起一张牌,然后将这张牌插入我们左手捏的手牌里面,在插入手牌之前,我们会自动计算将牌插入什么位置,然后将牌插入到这个计算后的位置,虽然这个计算转瞬而过,但我们还是尝试分析一下这个过程:
我决定摸起牌后,最小的牌放在左边,摸完后,牌面是从左到右依次增大
摸起第1张牌,直接捏在手里,现在还不用排序
摸起第2张牌,查看牌面大小,如果第二张牌比第一张牌大,就放在右边
摸起第3张牌,从右至左开始计算,先看右边的牌,如果摸的牌比最右边的小,那再从右至左看下一张,如果仍然小,继续顺延,直到找到正确位置(循环)
摸完所有的牌,结束
所以我们摸完牌,牌就已经排完序了。讲起来有点拗口,但是你在打牌的时候绝对不会觉得这种排序算法会让你头疼。这就是传说中的插入排序。
想象一下,假如我们认为左手拿的牌和桌面的牌堆就是同一数组,当我们摸完牌以后,我们就完成了对这个数组的排序。
【示例】
上图就是插入排序的过程,我们把它想象成摸牌的过程。
格子上方的数字:表示格子的序号,图(a)中,1号格子内的数字是5,2号格子是2,3号格子是4,以此类推
灰色格子:我们手上已经摸到的牌
黑色格子:我们刚刚摸起来的牌
白色格子:桌面上牌堆的牌
1、图(a),我们先摸起来一张5,然后摸起来第二张2,发现2比5小,于是将5放到2号格子,2放到1号格子(简单的人话:将2插到5前面)
2、图(b),摸起来一张4,比较4和2号格子内的数字5,4比5小,于是将5放到3号格子,再比较4和1号格子内的2,4大于2,4小于5,于是这就找到了正确的位置。(说人话:就是摸了张4点,将4和5交换位置)
3、图(c)、图(d)、图(e)和图(f),全部依次类推,相信打牌的你能够看懂。
看到这里,我相信应该没人看不懂什么是插入排序了,那么插入排序的代码长什么模样:
【代码实现】
public class InsertionSort {
public static void main(String【】 args) {
int【】 arr = {1, 2, 3, 7, 5, 2, 3, 3, 1};
System.out.println("排序前:");
for (int num : arr) {
System.out.print(num + " ");
}
//插入排序
for (int i = 1; i < arr.length; i++) {
for (int j = i - 1; j >= 0 && arr【j】 > arr【j + 1】; j--) {
swap(arr, j, j + 1);
}
}
System.out.println();
System.out.println("排序后:");
for (int num : arr) {
System.out.print(num + " ");
}
}
public static void swap(int【】 arr, int i, int j) {
int tmp = arr【i】;
arr【i】 = arr【j】;
arr【j】 = tmp;
}
}
【时间复杂度】
最好情况下,数组已经是有序的,每插入一个元素,只需要考查前一个元素,因此最好情况下,插入排序的时间复杂度为O(N)。
在最坏情况下,数组完全逆序,插入第2个元素时要考察前1个元素,插入第3个元素时,要考虑前2个元素,……,插入第N个元素,要考虑前 N - 1 个元素。因此,最坏情况下的比较次数是 1 + 2 + 3 + ... + (N - 1),等差数列求和,结果为 N2 / 2,所以最坏情况下的复杂度为 O(N2)。
当数据状况不同,产生的算法流程不同的时候,一律按最差的估计,所以插入排序是O(N2)的算法。
四、归并排序(MERGE-SORT)
【基本思想】
归并排序(MERGE-SORT)是利用归并的思想实现的排序方法,该算法采用经典的分治(divide-and-conquer)策略(分治法将问题分(divide)成一些小的问题然后递归求解,而治(conquer)的阶段则将分的阶段得到的各答案"修补"在一起,即分而治之)。
可以看到这种结构很像一棵完全二叉树,本文的归并排序我们采用递归去实现(也可采用迭代的方式去实现)。分阶段可以理解为就是递归拆分子序列的过程,递归深度为log2n。
【合并相邻有序子序列】
再来看看治阶段,我们需要将两个已经有序的子序列合并成一个有序序列,比如上图中的最后一次合并,要将【4,5,7,8】和【1,2,3,6】两个已经有序的子序列,合并为最终序列【1,2,3,4,5,6,7,8】,来看下实现步骤。
【代码实现】
public class MergeSort {
public static void mergeSort(int【】 arr) {
if (arr == null || arr.length < 2) {
return;
}
sortProcess(arr, 0, arr.length - 1);
}
public static void sortProcess(int【】 arr, int L, int R) {
if (L == R) {
return;
}
//L和R中点的位置,相当于(L+R)/2
int mid = L + ((R - L) ] 1);
//左边归并排序,使得左子序列有序
sortProcess(arr, L, mid);
//右边归并排序,使得右子序列有序
sortProcess(arr, mid + 1, R);
//将两个有序子数组合并操作
merge(arr, L, mid, R);
}
public static void merge(int【】 arr, int L, int mid, int R) {
int【】 temp = new int【R - L + 1】;
int i = 0;
//左序列指针
int p1 = L;
//右序列指针
int p2 = mid + 1;
while (p1 <= mid && p2 <= R) {
temp【i++】 = arr【p1】 < arr【p1】 ? arr【p1++】 : arr【p2++】;
}
////代码效果参考:http://hnjlyzjd.com/hw/wz_24976.html
两个必有且只有一个越界,即以下两个while只会发生一个//p1没越界,潜台词是p2必越界
while (p1 <= mid) {
//将左边剩余元素填充进temp中
temp【i++】 = arr【p1++】;
}
while (p2 <= R) {
//将右序列剩余元素填充进temp中
temp【i++】 = arr【p2++】;
}
//将辅助数组temp中的的元素全部拷贝到原数组中
for (int j = 0; j < temp.length; j++) {
arr【L + j】 = temp【j】;
}
}
public static void main(String【】 args) {
int【】 arr = {9, 8, 7, 6, 5, 4, 3, 2, 1};
mergeSort(arr);
System.out.println(Arrays.toString(arr));
}
}
【时间复杂度】
根据归并排序的流程,可以看出整个流程的时间复杂度的表达式为:T(N)=2T(N/2)+O(N),所以归并排序的时间复杂度//代码效果参考:http://hnjlyzjd.com/hw/wz_24974.html
为O(N logN)五、快速排序
快速排序由C. A. R. Hoare在1962年提出。它的基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
首先来了解一下经典快排:
5.1 经典快速排序
其中就小于等于的区域可以优化一下,小于的放小于区域,等于的放等于区域,大于的放大于区域。这就演变成荷兰国旗问题了。
【荷兰国旗问题】
给定一个数组arr,和一个数num,请把小于num的数放在数组的左边,等于num的数放在数组的中间,大于num的数放在数组的 右边。
大致过程如图:
当前数小于num时,该数与小于区域的下一个数交换,小于区域+1;当前数等于num时,继续比较下一个;当前数大于num时,该数与大于区域的前一个数交换,指针不变,继续比较当前位置。
代码如下:
public class NetherlandsFlag {
public static int【】 partition(int【】 arr, int L, int R, int num) {
int less = L - 1;
int more = R + 1;
int cur = L;
while (cur [span style="color: rgba(0, 0, 0, 1)"> more) {
if (arr【cur】 [span style="color: rgba(0, 0, 0, 1)"> num) {
//当前数小于num时,当前数和小于区域的下一个数交换,然后小于区域扩1位置,cur往下跳
swap(arr, ++less, cur++);
} else if (arr【cur】 > num) {
//当前数大于num时,大于区域的前一个位置的数和当前的数交换,且当前数不变,继续比较
swap(arr, --more, cur);
} else {
//当前数等于num时,直接下一个比较
cur++;
}
}
//返回等于区域的范围
//less+1是等于区域的第一个数
//more-1是等于区域的最后一个数
return new int【】{less + 1, more - 1};
}
public static void swap(int【】 arr, int i, int j) {
int tmp = arr【i】;
arr【i】 = arr【j】;
arr【j】 = tmp;
}
// for test
public static int【】 generateArray() {
int【】 arr = new int【10】;
for (int i = 0; i < arr.length; i++) {
arr【i】 = (int) (Math.random() 3);
}