在学习Java集合框架的过程中,我了解到Arrays工具类,其中提供了许多多态的sort方法,这些方法无一例外底层都用到了双轴快排(DualPivotQuicksort),那么我们学过的多种排序算法和它相比,有什么缺点又有哪些亮点呢?今天我们一起回顾一下吧。(双轴快排的代码我还没懂,堆排还没学到,期待下次更新)。
冒泡排序
冒泡简直是我平时排序的首选,因为它只需要写两层for外加定界就搞定了。其代码简单来自于逻辑简单,我们看图例:
代码如下:
public List<Integer> bubbleSort(List<Integer> list) {
int size = list.size();
for (int i = 0; i < size - 1; i++) {
for (int j = 0; j < size - 1 - i; j++) {
if (list.get(j) > list.get(j + 1)) {
int temp = list.get(j);
list.set(j, list.get(j + 1));
list.set(j + 1, temp);
}
}
}
return list;
}
逻辑简单,标准的O(n^2),具有稳定性。
插入排序
插入排序的主要思想是:维护一个有序的数组,后来加入的元素也要确保原数组的有序性。很容易想到外层用for添加每一个元素,内层再来for判断在哪个位置加入。 代码如下:
public List<Integer> insertSort(List<Integer> list) {
int size = list.size();
int startNum = list.get(0);
for (int i = 1; i < size; i++) {
for (int j = i; j > 0; j--) {
if (list.get(j - 1) > list.get(j)) {
int temp = list.get(j - 1);
list.set(j - 1, list.get(j));
list.set(j, temp);
}
}
}
return list;
}
也是标准O(n^2),具有稳定性(但如果判断语句使用>=则不具有稳定性)。
选择排序
选排的主要思想也很简单很暴力,次次从元素堆里选择最小的元素,添加到有序数组后更新元素堆。
代码如下:
public List<Integer> choiceSort(List<Integer> list) {
int size = list.size();
for (int i = 0; i < size - 1; i++) {
int minNum = list.get(size - 1);
int location = size - 1;
for (int j = i; j < size - 1; j++) {
if (list.get(j) < minNum) {
minNum = list.get(j);
location = j;
}
}
int temp = list.get(i);
list.set(i, minNum);
list.set(location, temp);
}
return list;
}
标准的O(n^2),也具有稳定性。
希尔排序
我总结的希尔排序的逻辑是:先通过分组插排降低逆序,最后进行完整的插入排序消除逆序。
其中有点分治的思想,也有点归并的意思,下面我们一起看看流程:
代码如下:
public List<Integer> shellSort(List<Integer> list) {
int size = list.size();
for (int gap = size / 2; gap >= 1; gap /= 2) {
//最外层循环确定循环次数:每次减半的gap循环一次直到gap=1;
for (int i = gap; i < size; i++) {
//对每一个分组进行插排,因为我下面的插排是倒着往前插,所以最左边的一部分就不考虑了。
int tempFanhui = i - gap;
//记录一个变量用于判断组内前面有无元素可插入。
while (tempFanhui >= 0) {
//当前面有元素
if (list.get(tempFanhui) > list.get(tempFanhui+gap)) {
//下面就是换位了
int temp = list.get(tempFanhui+gap);
list.set(tempFanhui+gap, list.get(tempFanhui));
list.set(tempFanhui, temp);
}
tempFanhui-=gap;
}
}
}
return list;
}
我们仔细看,其实最外层最后一次循环(gap=1),就是进行了一次完整的插排,只不过和一般插排而言其内部更加有序,意味着大部分情况只需要比较不需要换位。在其他人的分析和实际的测试中发现,这样的时间复杂度确实下降了,并且一切操作都在数组内完成,只需要几个局部变量,空间复杂度也是常数。
快排
快排的主要思想是分治,局部有序。
其具体实现是通过选择某个值将原数组分成大于它和小于它两份,在这两份中再次调用此方法来降逆序,直到分组的长度为1的时候就无需比较,完成排序。
// public List<Integer> quickSort(List<Integer> list, int fromNum, int toNum) {
// int size = toNum - fromNum;
// if (size <= 1) {
// return list;
// }
// int pivot = list.get(fromNum + size / 2);
// int i = fromNum, j = toNum - 1;
// while (i <= j) {
// while (list.get(i) < pivot) {
// i++;
// }
// while (list.get(j) > pivot) {
// j--;
// }
// if (i <= j) {
// Collections.swap(list, i, j);
// i++;
// j--;
// }
// }
// quickSort(list, fromNum, j + 1);
// quickSort(list, i, toNum);
// return list;
//
// }
代码中最难的一点是:如何将元素根据大小安置在数组的两侧,以上代码给出的思路是从左边找一个大的,右边找一个小的交换位置,直到二者能够顺利相遇,就意味着两侧已经没有大小相反的元素了。
我也有一个想法:(对空间复杂度很不友好)我原本选的pivot是最右边的元素,然后数组从最左边开始遍历,大的元素加到原数组最右边,原位置设置为null,一次遍历以后删除所有的null元素。这种做法很明显需要额外的内存空间,如果是对于动态数组的话,如果添加后size>length,那底层的自动扩容和重新创建一个新的更长的数组没有区别,反而最后因为size不变,会浪费掉额外增加的长度。
代码如下,留作以后复盘(最主要还有bug没de出来,但马上要忙其他东西,以后有机会改正)。
public List<Integer> quickSort(List<Integer> list,int fromNum,int toNum){
int size = toNum - fromNum;
if (size<=1) {
return list;
}
int pivot = list.get(size - 1);
int nullNum=0;
for (int i = 0; i < size; i++) {
if(list.get(i)>pivot){
list.add(toNum,list.get(i));
list.set(i,null);
nullNum++;
}
}
for (int i = 0; i < nullNum; i++) {
list.remove(null);
}
int pivotNum=list.indexOf(pivot);
quickSort(list, fromNum, pivotNum);
quickSort(list, pivotNum , toNum);
return list;
}
归并排序
归并的主要思路是:在递归种,分治排序,然后用简单的方法合并两个有序数组。
代码如下:
public List<Integer> merge(List<Integer> list) {
if (list.size() <= 1) {
return list;
}
int mid = list.size() / 2;
List<Integer> list1 = list.subList(0, mid);
List<Integer> list2 = list.subList(mid, list.size());
List<Integer> list3 = merge(list1);
List<Integer> list4 = merge(list2);
Queue<Integer> queue = new ArrayDeque<>();
Queue<Integer> queue1 = new ArrayDeque<>();
queue.addAll(list3);
queue1.addAll(list4);
List<Integer> list5 = new ArrayList<>();
int i = 0;
while (i < list.size()) {
if (!queue.isEmpty() && !queue1.isEmpty()) {
if (queue.peek() >= queue1.peek()) {
list5.add(queue1.poll());
i++;
} else {
list5.add(queue.poll());
i++;
}
} else {
if (queue.isEmpty()) {
list5.addAll(queue1);
break;
}
if (queue1.isEmpty()) {
list5.addAll(queue);
break;
}
}
}
return list5;
}
public List<Integer> mergeSort(List<Integer> list) {
int gap = list.size();
return merge(list);
}
代码有不成熟的点是:开辟了太多新空间了。
代码关键的一点是如何将两个有序的数组合并,我选择用队列存储这两个数组(当然在原本的list中只要限制好方法就可以),每次比较首位的大小,将小的取出添加到新数组中,重复以上操作。
总结
初学者入门,而且时间有点紧,许多算法分析的不够深入。
我记录下下一个版本要做的更新:
1:堆排
2:二分法插排
3:改进的冒泡排序
4:各种算法的复杂度分析
5:简析双轴快排和Arrays工具类