冒泡排序
冒泡排序是重复地走访过要排序的元素列,依次比较两个相邻的元素,如果顺序(你所定义的逻辑顺序)错误就把他们交换过来。
走访元素的工作是重复地进行直到没有相邻元素需要交换,也就是说该元素列已经排序完成。
1)冒泡排序只会操作相邻的两个数据。
2)对相邻两个数据进行比较,看是否满足大小关系要求,若不满足让它俩互换。
3)一次冒泡会让至少一个元素移动到它应该在的位置,重复n次,就完成了n个数据的排序工作。
冒泡排序的执行效率
最好情况下时间复杂度
最好情况为当要排序的数据已经是有序的,只需要进行一次冒泡排序,时间复杂度为O(n)。
最坏情况下时间复杂度
最坏情况为当要排序的数据是倒序的,我们需要进行n次冒泡操作,时间复杂度为O(n²)。
平均情况下时间复杂度
冒泡排序的平均情况下时间复杂度为O(n²)
对于包含 n 个数据的数组,这 n 个数据就有 n! 种排列方式。
不同的排列方式,冒泡排序执行的时间肯定是不同的。可以利用有序度和逆序度来分析。
有序度、无序度、满有序度
有序度是数组中具有有序关系的元素对的个数
有序元素对:a[i] <= a[j], 如果i < j。
反之就是无序度
逆序元素对:a[i] > a[j], 如果i < j。
完全有序的数组的有序度叫做满有序度,满有序度的元素对有n*(n-1)/2个。
2,4,3,1,5,6的有序度为11,满有序度为6*(6-1)/2=15, 可以得到:逆序度 = 满有序度 - 有序度
其实排序的过程也可以理解成一种增加有序度,减少逆序度的过程,最后达到满有序度,就说明排序完成了。
冒泡排序中每交换一次,有序度就+1
最坏情况下,初始状态的有序度是 0,所以要进行 n*(n-1)/2 次交换
最好情况下,初始状态的有序度是 n*(n-1)/2,就不需要进行交换。
我们可以取个中间值 n*(n-1)/4,来表示初始有序度既不是很高也不是很低的平均情况。
也就是说,平均情况下要进行 n*(n-1)/4 次交换操作,比较操作肯定要比交换操作多,而复杂度的上限是 O(n2),所以平均情况下的时间复杂度就是 O(n2)。
冒泡排序的内存消耗
冒泡排序的排序过程只涉及相邻数据的比较和交换操作,只需要常量级的临时空间,空间复杂度为O(1),属于原地排序。
冒泡排序的稳定性
在冒泡排序中,只有交换才可以改变两个元素的前后顺序。
为了保证冒泡排序算法的稳定性,当有相邻的两个元素大小相等的时候,我们不做交换,相同大小的数据在排序前后不会改变顺序,所以冒泡排序是稳定的排序算法。
冒泡排序的代码实现
public void bubbleSort(int[] a, int n) {
if (n <= 1) return;
for (int i = 0; i < n - 1; i++) {//外层循环:排好N个数据,需要排N-1次,每循环一次,排好一个数据
boolean isExchange = false;
for (int j = 0; j < n - 1 - i; j++) { //内层循环,排好一个数据,需要比较N-1-i次
//i为已经循环过的次数(已经排好的数据个数)
if (a[j] > a[j + 1]) {
int bigger = a[j];
a[j] = a[j + 1];
a[j + 1] = bigger;
isExchange = true; //发生数据交换
}
}
if (!isExchange) break; //遍历未排序的部分,未发生数据交换,说明数组已经有序,跳出
}
}
冒泡排序的代码再优化
思路:在每一轮排序后记录最后一次元素交换的位置,作为下次比较的边界,对于边界外的元素在下次循环中无需比较。
public static void bubbleSort2(int[] a, int n) {
if (n <= 1) return;
// 最后一次交换的位置
int lastExchange = 0;
// 无序数据的边界,每次只需要比较到这里即可退出
int sortBorder = n - 1;
for (int i = 0; i < n - 1; i++) {
boolean isExchange = false;
for (int j = 0; j < sortBorder; j++) {
if (a[j] > a[j + 1]) {
int tmp = a[j];
a[j] = a[j + 1];
a[j + 1] = tmp;
// 发生数据交换
isExchange = true;
// 更新最后一次交换的位置
lastExchange = j;
}
}
sortBorder = lastExchange;
if (!isExchange) break; // 没有数据交换,说明数组已经有序,跳出
}
}