冒泡排序
最初版
一趟确定一个最大的数排在了末尾,外层循环控制趟数,内层循环控制次数。
private static void sort(int[] arr) {
int count =0;
//最后一轮不需要排序 所以到arr.length-2位置的索引即可
for (int i = 0; i < arr.length - 1; i++) {
//每轮过后都可以确定一个数 所以arr.length-(i+1)
for (int j = 0; j < arr.length - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
arr[j + 1] ^= arr[j];
arr[j] ^= arr[j + 1];
arr[j + 1] ^= arr[j];
count++;
}
}
}
System.out.println("共交换了:"+count);
}
时间复杂度:无论如何都将进行两次for循环,O(n^2)
升级版
相比于初级版,这一版设置了标志位,如果一趟之后发现没有元素进行交换,即证明后面已经有序,就没有必要进行后面的循环了。
private static void sort1(int[] arr) {
int count = 0;
for (int i = 0; i < arr.length - 1; i++) {
//优化排序,增加判断位,有序标记
boolean flag = true;
for (int j = 0; j < arr.length - 1 - i; j++) {
//异或运算交换元素
if (arr[j] > arr[j + 1]) {
arr[j + 1] ^= arr[j];
arr[j] ^= arr[j + 1];
arr[j + 1] ^= arr[j];
//有元素交换,所以还需要进行下一轮排序
flag = false;
count++;
}
}
//该轮结束后,如果顺序没改动说明已经排序结束
if (flag) {
System.out.println("!!!!!后面已经有序!!!");
break;
}
}
System.out.println("共交换了:"+count);
}
时间复杂度:
- 最好的情况:都有序的情况,O(n)
- 最坏的情况:逆序,O(n^2)
最终版
相比于上一版本,设置了最后一次交换的位置,因为冒泡排序可以确定的是,每一趟之后都会有至少一个确定位置,那么确定的位置即可不用参与判断。
private static void sort2(int[] arr) {
int count = 0;
// 最后一次交换的下标
int lastSwapIndex = 0;
// 无序数组的边界,每次比较比到这里为止
int arrBoundary = arr.length - 1;
for (int i = 0; i < arr.length - 1; i++) {
// 优化冒泡排序,增加判断位,有序标记,每一轮的初始是true
boolean flag = true;
for (int j = 0; j < arrBoundary; j++) {
// 找最小数,如果前一位比后一位大,则交换位置
if (arr[j] > arr[j + 1]) {
arr[j + 1] ^= arr[j];
arr[j] ^= arr[j + 1];
arr[j + 1] ^= arr[j];
// 有元素交换,所以不是有序,标记变为false
flag = false;
// 最后一次交换元素的位置
lastSwapIndex = j;
count++;
}
}
// 把最后一次交换元素的位置赋值给无序数组的边界
arrBoundary = lastSwapIndex;
// 说明上面内层for循环中,没有交换任何元素,直接跳出外层循环
if (flag||arrBoundary == 0) {
break;
}
}
System.out.println("共交换了:"+count);
}
时间复杂度:
- 最好的情况下:仍然是有序情况,依然会进行第一个循环,O(n)。
- 最坏的情况下:逆序,O(n^2)。
插入排序
插入排序类似于抓牌的过程,先抓一张6到手上,此时再抓一张3,有意识地和6进行比较,发现3比6小,那么就可以将3插到6的前面。
插入的细节:将大的位置向后移动一格,将小的插入。
//插入排序:先前的子线性表都保证是有序的 如果将[1,3,2]进行按递增排序,如何将2插入呢
//假设子线性表有序,先将2存到变量current中,向前比较,如果比之前小,则arr[2] = arr[1],此时变成[1,3,3]
//直到没有前面元素大为止,此时将当前位置赋值为current,arr[1] = current = 2,结果便是[1,2,3]
private static void insertSort(int[] arr){
//每一轮抽一个数
for(int i = 1;i<arr.length;i++){
int current = arr[i];
int k;
for(k = i-1;k>=0&& arr[k]>current;k--){
//如果抽到的这个数比之前的小,那么将之前的向后挪动
arr[k+1] = arr[k];
}
//将抽到的这个数插入
arr[k+1] = current;
}
}
时间复杂度:
- 最好的情况:想当于摸上的牌正好有序,O(n)。
- 最坏的情况:正好逆序抓牌,每一次摸牌都必须逐一和之前的牌进行比较,O(n^2)
总结
我们可以发现,冒泡排序和插入排序都是依靠交换相邻元素的位置来达到排序的目的,且必须是严格大于或者严格小于的情况下才会交换,他们都是稳定的排序。
更具体一些,他们其实都是为了消除逆序对,而进行依次相邻元素的交换,正好可以消去一个逆序对。
逆序对:如果下标i<j满足,arr[i]>arr[j],(i,j)就称为一对逆序对。
可以测试一下,如果传入[8,3,2,9,1],总共有七组逆序对:(8,3),(8,2),(8,1),(3,2),(3,1),(2,1),(9,1),因此冒泡和插入排序都将交换七次。
因此对于插入排序而言,T(N,I) = O(N,I),它消耗的时间其实逆序对个数的影响,如果序列基本有序,插入排序将非常适合,因为简单且高效。(ps:但往往真实情况下,序列都将是无序的,因此我们需要另外的排序方式,以求交换一次就可以消除很多对逆序对。)