原序列 | 3 | 9 | 6 | 5 | 8 | 2 | 7 | 4 |
---|---|---|---|---|---|---|---|---|
第1趟 | 3 | 6 | 5 | 8 | 2 | 7 | 4 | 9 |
第2趟 | 3 | 5 | 6 | 2 | 7 | 4 | 8 | 9 |
第3趟 | 3 | 5 | 2 | 6 | 4 | 7 | 8 | 9 |
第4趟 | 3 | 2 | 5 | 4 | 6 | 7 | 8 | 9 |
第5趟 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
第6趟 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
第7趟 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
如表格所示,每一趟都将当前乱序序列中最大的数移到尾端。【小伙伴们从表格中看出基本冒泡排序能够优化的地方了吗?】下面先来基本实现代码。java
java实现冒泡排序:算法
private static <T extends Comparable<? super T>> void bubbleSort(T[] nums) {
if (null == nums || nums.length == 0) {
throw new RuntimeException("数组为null或长度为0");
}
T temp = null;
int length = nums.length;
//外循环是趟数,每一趟都会将未排序中最大的数放到尾端
for (int i = 0; i < length - 1; i++) {
//内循环是从第一个元素开始,依次比较相邻元素,
// 比较次数随着趟数减小,由于每一趟都排好了一个元素
for (int j = 0; j < length - 1 - i; j++) {
if (nums[j].compareTo(nums[j + 1]) > 0) {
temp = nums[j];
nums[j] = nums[j + 1];
nums[j + 1] = temp;
}
}
}
}
从表格中,相信小伙伴已经看出,在第5趟其实已经排好序了,但基本的冒泡排序算法还会进行第7趟比较,这其实只是进行不必的比较,而不会进行元素的交换。(第6趟仍是必需要走的,下面会说明)数组
时间复杂度:因为内外循环都发生N次迭代,因此时间复杂度为O(n^2)。而且这个界是精确的。思考最坏的状况,输入一个逆序的数组,则比较次数为:app
(N-1)+(N-2)+(N-3)+..+2+1 = n*(n-1)/2 = O(n^2)优化
空间复杂度:只使用了一个临时变量,因此为O(1);spa
是否稳定:稳定排序code
咱们换个角度看待这个问题。基本冒泡算法之因此进行了无用的多余扫描,是由于不知道已经排好了序;因此只要咱们在第 i 趟(i小于N-1)就知道序列已经排好序,咱们就不用进行以后的扫描了。orm
综上所述,咱们能够增长一个boolean变量,来标识是否已经排好序。优化代码以下:排序
冒泡排序优化普通版:ci
private static <T extends Comparable<? super T>> void bubbleSort(T[] nums) {
if (null == nums || nums.length == 0) {
throw new RuntimeException("数组为null或长度为0");
}
T temp = null;
int length = nums.length;
//用于标识是否已经将序列排好序
boolean isOrdered = false;
for (int i = 0; i < length - 1; i++) {
//每一趟开始前都假设已经有序
isOrdered = true;
for (int j = 0; j < length - 1 - i; j++) {
if (nums[j].compareTo(nums[j + 1]) > 0) {
temp = nums[j];
nums[j] = nums[j + 1];
nums[j + 1] = temp;
//若是出现有元素交换,则代表此躺可能没有完成排序
isOrdered = false;
}
}
//若是当前趟都没有进行元素的交换,证实前面一趟比较已经排好序
//直接跳出循环
if (isOrdered) {
break;
}
}
}
注意:虽然第5趟已经排好序,但对于程序来讲,它并不知道此趟已经排好序,须要进行下一趟扫描来肯定上一趟是否已经将原序列排好序。因此第6趟是必需要去扫描的。
你觉得结束了吗?哈哈哈,尚未,这只是初版优化。
让咱们想想这样的状况。对于下列序列,前半部分乱序,后半部分有序。
原序列 | 4 | 5 | 3 | 2 | 6 | 7 | 8 | 9 |
---|---|---|---|---|---|---|---|---|
第一趟 | 4 | 3 | 2 | 5 | 6 | 7 | 8 | 9 |
第二趟 | 3 | 2 | 4 | 5 | 6 | 7 | 8 | 9 |
第三趟 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
简述排序过程:
第一趟:发生交换的是5和3,接着是5和2;随后5与6比较,不须要换位置,相同地,6与七、7与八、8与9都不须要更换位置。因此第一趟结果为:[4,3,2,5,6,7,8,9]。
第二趟:发生交换的是4与3,接着4与2;随后4与五、5与6,6与七、7与8都不须要更换位置。【8不须要与9比较,由于第一趟已经将最大的数下沉到尾端】。因此第二趟结果为:[3,2,4,5,6,7,8,9]。
第三趟:发生交换的是3与2;随后3与4,4与5,5与6,6与7都不须要更换位置。因此第三趟结果为:[2,3,4,5,6,7,8,9]。
你们看出什么了吗?其实进行了不少无心义的比较,由于这些都不须要更换位置,而不少趟都会重复比较。根据冒泡排序思想,咱们知道,有序序列长度,其实跟排序趟数相等,每一趟就是将当前乱序中的最大值下沉到数组尾端。但其实序列真正有序的序列长度是大于当前排序趟数的。也就是说,只要咱们找到了原序列中无序与有序的边界,就能够避免再去比较有序序列。
其实最后一次交换的位置,就是无序序列与有序序列的边界。
从例子中看:
第一趟最后一次交换的位置是元素5与2交换的位置,即数组下标2的位置;
第二趟最后一次交换的位置是元素4与2交换的位置,即数组下标1的位置;
第三趟最后一次交换的位置是元素3与2交换的位置,即数组下标0的位置;
因此,只要咱们记录下当前趟最后一次交换的位置,在下一趟只比较到这个位置便可。
冒泡排序优化增强版:
private static <T extends Comparable<? super T>> void bubbleSort(T[] nums) {
if (null == nums || nums.length == 0) {
throw new RuntimeException("数组为null或长度为0");
}
T temp = null;
int length = nums.length;
boolean isOrdered = false;
int lastExchangeIndex = 0;
//当前趟无序的边界
int unorderedBorder = length - 1;
for (int i = 0; i < length - 1; i++) {
//每一趟开始前都假设已经有序
isOrdered = true;
for (int j = 0; j < unorderedBorder; j++) {
if (nums[j].compareTo(nums[j + 1]) > 0) {
temp = nums[j];
nums[j] = nums[j + 1];
nums[j + 1] = temp;
//若是出现有元素交换,则代表此躺没有完成排序
isOrdered = false;
//记录下最后一次交换元素的位置
lastExchangeIndex = j;
}
}
unorderedBorder = lastExchangeIndex;
if (isOrdered) {
break;
}
}
}
其实,还能够进一步优化, 有兴趣的能够去看看鸡尾酒排序,咱们已经很接近了。
冒泡排序能够经过增长boolean标识是否已经排好序来进行优化;还能够记录下最后一次交换元素的位置来进行优化,防止无心义的比较。冒泡排序是稳定排序,时间复杂度为O(n^2),空间复杂度为O(1)。