数据结构中的六种排序(Java语言)

35 阅读3分钟

在学习Java集合框架的过程中,我了解到Arrays工具类,其中提供了许多多态的sort方法,这些方法无一例外底层都用到了双轴快排(DualPivotQuicksort),那么我们学过的多种排序算法和它相比,有什么缺点又有哪些亮点呢?今天我们一起回顾一下吧。(双轴快排的代码我还没懂,堆排还没学到,期待下次更新)。

冒泡排序

冒泡简直是我平时排序的首选,因为它只需要写两层for外加定界就搞定了。其代码简单来自于逻辑简单,我们看图例:

image.png

代码如下:

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),具有稳定性。

插入排序

image.png

插入排序的主要思想是:维护一个有序的数组,后来加入的元素也要确保原数组的有序性。很容易想到外层用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),具有稳定性(但如果判断语句使用>=则不具有稳定性)。

选择排序

选排的主要思想也很简单很暴力,次次从元素堆里选择最小的元素,添加到有序数组后更新元素堆。

image.png

代码如下:

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),也具有稳定性。

希尔排序

我总结的希尔排序的逻辑是:先通过分组插排降低逆序,最后进行完整的插入排序消除逆序。

其中有点分治的思想,也有点归并的意思,下面我们一起看看流程:

image.png

代码如下

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的时候就无需比较,完成排序。

image.png

//    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;
}

归并排序

归并的主要思路是:在递归种,分治排序,然后用简单的方法合并两个有序数组。

image.png 代码如下:

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工具类