排序算法总结

排序大的分类可以分为两种:内排序和外排序。在排序过程中,全部记录存放在内存,则称为内排序。如果排序过程中需要使用外存,则称为外排序。下面讲的排序都是属于内排序。 


一、插入排序

(一)、直接插入排序

public class Sort {
    public static void main(String[] args) {
        int[] arr = {1, 3, 51, 65, 6, 34, 67, 343, 56};
        int[] sortResult = insertionSort(arr);
        System.out.println(Arrays.toString(sortResult));
    }

    /**
     * 用一个临时变量存储待插入的值,从后往前找,如果找到比这个值大的元素,则将其前面的元素依次后移,
     * 结束后再将带插入的值放到该插入的位置,减去了许多不必要的交换操作
     * @param arr
     * @return
     */
    public static int[] insertionSort(int[] arr) {
        for (int i = 1; i < arr.length; i++) {
            //待插入元素
            int insertValue = arr[i];
            int preIndex = i - 1;
            while (preIndex >= 0 && insertValue < arr[preIndex]) {
                arr[preIndex + 1] = arr[preIndex];
                preIndex--;
            }
            arr[preIndex + 1] = insertValue;
        }
        return arr;
    }
}

(二)、希尔排序(shell排序)

public class Sort {
    public static void main(String[] args) {
        int[] arr = {1, 3, 51, 65, 6, 34, 67, 343, 56};
        int[] sortResult = shellSort(arr);
        System.out.println(Arrays.toString(sortResult));
    }

    /**
     * 希尔排序
     * 插入排序的升级版,设定步长为数组长度的一半,每次都除以二,直到步长为1
     * @param arr
     * @return
     */
    public static int[] shellSort(int[] arr) {
        int len = arr.length;
        for (int k = len / 2; k > 0; k /= 2) {
            for (int i = k; i < len; i++) {
                int temp = arr[i];
                int j = i - k;
                while (j >= 0 && temp < arr[j]) {
                    arr[j + k] = arr[j];
                    j -= k;
                }
                arr[j + k] = temp;
            }
        }
        return arr;
    }
}

二、选择排序

(一)、简单选择排序(直接排序)

public class Sort {
    public static void main(String[] args) {
        int[] arr = {1, 3, 51, 65, 6, 34, 67, 343, 56};
        int[] sortResult = selectionSort(arr);
        System.out.println(Arrays.toString(sortResult));
    }

    /**
     * 遍历数组,每次遍历都找到最小的元素,记录其下标,内层循环结束后再根据下标将其与数组头部元素交换
     * 与冒泡排序不同的是,冒泡排序每次循环可能交换多次,而选择排序最多交换一次
     * @param arr 待排序数组
     * @return
     */
    public static int[] selectionSort(int[] arr) {
        int temp, minIndex;
        for (int i = 0; i < arr.length - 1; i++) {
            minIndex = i;
            for (int j = i + 1; j < arr.length; j++) {
                if (arr[j] < arr[minIndex]) {
                    minIndex = j;
                }
            }
            temp = arr[minIndex];
            arr[minIndex] = arr[i];
            arr[i] = temp;
        }
        return arr;
    }
}

(二)、堆排序

public class HeapSort {

    public static void main(String[] args) {
        int[] arr = {1, 3, 51, 65, 6, 34, 67, 343, 56};
        int[] sortResult = heapSort(arr);
        System.out.println(Arrays.toString(sortResult));
    }

    public static int[] heapSort(int[] arr) {
        //以最后一个非叶子结点构建大顶堆
        for (int i = arr.length / 2 - 1; i >= 0; i--) {
            adjustHeap(arr, i, arr.length);
        }
        //此时顶部元素是最大的,交换顶部元素和末端元素
        for (int i = arr.length - 1; i > 0; i--) {
            swap(arr, 0, i);
            //末端元素已经是最大的了,无需考虑排序
            adjustHeap(arr, 0, i);
        }
        return arr;
    }

    /**
     * 形成大顶堆
     *
     * @param arr 数组元素
     * @param i   当前结点位置
     * @param len 结点个数
     */
    public static void adjustHeap(int[] arr, int i, int len) {
        //保存当前结点
        int temp = arr[i];
        //遍历当前结点的左子结点
        for (int k = 2 * i + 1; k < len; k = 2 * k + 1) {
            //如果右结点存在 且 右结点比左结点大,指向右结点
            if (k + 1 < len && arr[k] < arr[k + 1]) {
                k++;
            }
            //判断当前结点和左(右)结点哪个大
            if (temp < arr[k]) {
                //交换
                swap(arr, k, i);
                //交换后,下次遍历以该子结点作为根节点的子树就会受到影响,因此需要重新指定下次的根节点
                i = k;
            } else {
                //不用交换,直接终止循环
                break;
            }
        }
    }

    public static void swap(int[] arr, int i, int j) {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }
}

三、交换排序

(一)、冒泡排序

/**
 * 遍历数组,依次比较相邻的元素并交换,每次都将最大元素(根据正序还是逆序决定)放到数组末尾
 * @param arr 待排序数组
 * @return
 */
public static int[] bubbleSort(int[] arr) {
    int temp;

    for (int i = 0; i < arr.length - 1; i++) {
        for (int j = 0; j < arr.length - i - 1; j++) {
            if (arr[j] > arr[j + 1]) {
                temp = arr[j + 1];
                arr[j + 1] = arr[j];
                arr[j] = temp;
            }
        }
    }
    return arr;
}

冒泡排序优化一:

//冒泡排序:优化一
public static int[] bubbleSort1(int[] arr) {
    int temp;
    //记录数组是否有序
    boolean isSorted;

    for (int i = 0; i < arr.length - 1; i++) {
        isSorted = true;
        for (int j = 0; j < arr.length - i - 1; j++) {
            if (arr[j] > arr[j + 1]) {
                temp = arr[j + 1];
                arr[j + 1] = arr[j];
                arr[j] = temp;
                //记录本轮排序是否交换了元素,如果交换则置为false
                isSorted = false;
            }
        }
        //没有交换证明已经是有序的了,直接终止循环
        if (isSorted) {
            break;
        }
    }
    return arr;
}

冒泡排序优化二:

//冒泡排序:优化二
public static int[] bubbleSort2(int[] arr) {
    int temp;
    boolean isSorted;
    //第一次循环边界
    int sortBorder = arr.length - 1;
    //记录每轮排序最后一次进行交换的位置
    int lastSwapIndex = 0;

    for (int i = 0; i < arr.length - 1; i++) {
        isSorted = true;
        for (int j = 0; j < sortBorder; j++) {
            if (arr[j] > arr[j + 1]) {
                temp = arr[j + 1];
                arr[j + 1] = arr[j];
                arr[j] = temp;
                isSorted = false;
                lastSwapIndex = j;
            }
        }
        sortBorder = lastSwapIndex;
        if (isSorted) {
            break;
        }
    }
    return arr;
}

(二)、快速排序

算法原理:先在数组中选择一个数字,通过一趟排序将要排序的数据分割成独立的两部分:比选择的数字小的数字都移到数组的左边,比选择的数字大的数字都移到数组的右边。然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。

快速排序中基准的选取:
1. 第一个元素或最后一个元素,但是当数组中的元素原本为逆序时,升序排序就会变成一次只能确定基准元素的位置,无法发挥快排的优势
2. 随机选取
3. 三数取中法,也就是取左端、中间、右端三个数,然后进行排序,将中间数作为枢纽值。

举个栗子:

原数组:{3,7,2,9,1,4,6,8,10,5}
期望结果:{1,2,3,4,5,6,7,8,9,10}

快速排序示意图:

代码实现:

public class QuickSort {
    
    public static int divide(int[] a, int start, int end){
        //每次都以最右边的元素作为基准值
        int base = a[end];
        //start一旦等于end,就说明左右两个指针合并到了同一位置,可以结束此轮循环。
        while(start < end){
            while(start < end && a[start] <= base)
                //从左边开始遍历,如果比基准值小,就继续向右走
                start++;
            //上面的while循环结束时,就说明当前的a[start]的值比基准值大,应与基准值进行交换
            if(start < end){
                //交换
                int temp = a[start];
                a[start] = a[end];
                a[end] = temp;
                //交换后,此时的那个被调换的值也同时调到了正确的位置(基准值右边),因此右边也要同时向前移动一位
                end--;
            }    
            while(start < end && a[end] >= base)
                //从右边开始遍历,如果比基准值大,就继续向左走
                end--;
            //上面的while循环结束时,就说明当前的a[end]的值比基准值小,应与基准值进行交换
            if(start < end){
                //交换
                int temp = a[start];
                a[start] = a[end];
                a[end] = temp;
                //交换后,此时的那个被调换的值也同时调到了正确的位置(基准值左边),因此左边也要同时向后移动一位
                start++;
            }    
            
        }
        //这里返回start或者end皆可,此时的start和end都为基准值所在的位置
        return end;
    }
 
    /**
     * 排序
     * @param a
     * @param start
     * @param end
     */
    public static void sort(int[] a, int start, int end){
        if(start > end){ //start >= end ?
            //如果只有一个元素,就不用再排下去了
            return;
        } 
        else{
            //如果不止一个元素,继续划分两边递归排序下去
            int partition = divide(a, start, end);
            sort(a, start, partition-1);
            sort(a, partition+1, end);
        }
            
    }


    public static void main(String[] args) {
        // TODO Auto-generated method stub
        int[] a = new int[]{2,7,4,5,10,1,9,3,8,6};
        int[] b = new int[]{1,2,3,4,5,6,7,8,9,10};
        int[] c = new int[]{10,9,8,7,6,5,4,3,2,1};
        int[] d = new int[]{1,10,2,9,3,2,4,7,5,6};
        int[] e = {3};
            
        sort(b, 0, b.length-1);
            
        System.out.println("排序后的结果:");
        for(int x : b){
            System.out.print(x+" ");
        }

    }

}

四、归并排序

public class Sort {
    public static void main(String[] args) {
        int[] arr = {1, 3, 51, 65, 6, 34, 67, 343, 56};
        int[] sortResult = mergeSort(arr);
        System.out.println(Arrays.toString(sortResult));
    }

    //归并排序
    public static int[] mergeSort(int[] arr) {
        return mergeSort(arr, 0, arr.length - 1, new int[arr.length]);
    }

    /**
     * 归并排序通过递归将数组分解为只有两个元素,按照它们的大小放入到一个临时数组中,直到全部合并
     *
     * @param arr   待排序数组
     * @param left  左索引
     * @param right 右索引
     * @param temp  临时数组,存储每次合并后的元素
     * @return
     */
    public static int[] mergeSort(int[] arr, int left, int right, int[] temp) {
        if (left < right) {
            int mid = (left + right) >> 1;
            //向左分解
            mergeSort(arr, left, mid, temp);
            //向右分解
            mergeSort(arr, mid + 1, right, temp);
            //合并
            merge(arr, left, mid, right, temp);
        }
        return arr;
    }

    //合并
    public static int[] merge(int[] arr, int left, int mid, int right, int[] temp) {
        int i = left, j = mid + 1, k = 0;
        //按大小放入临时数组中
        while (i <= mid && j <= right) {
            if (arr[i] < arr[j]) {
                temp[k] = arr[i];
                k++;
                i++;
            } else {
                temp[k] = arr[j];
                k++;
                j++;
            }
        }

        //将剩余元素放到temp剩余位置
        while (i <= mid) {
            temp[k] = arr[i];
            k++;
            i++;
        }
        while (j <= right) {
            temp[k] = arr[j];
            k++;
            j++;
        }

        //将排好序的temp数组元素赋值给原数组
        k = 0;
        int l = left;
        while (l <= right) {
            arr[l] = temp[k];
            k++;
            l++;
        }

        return arr;
    }

}

五、基数排序

public class RadixSort {

    public static void main(String[] args) {
        int[] arr = {1, 3, 51, 65, 6, 34, 67, 343, 56};
        int[] sortResult = radixSort(arr);
        System.out.println(Arrays.toString(sortResult));
    }

    /**
     * 基数排序:
     * 根据每个数的个位、十位、百位...的值(0~9)放入桶中(规则和计数排序相同),因此需要10个桶
     *
     * @param arr
     * @return
     */
    public static int[] radixSort(int[] arr) {
        //创建并初始化10个桶
        ArrayList<LinkedList<Integer>> bucketList = new ArrayList<>(10);
        for (int i = 0; i < 10; i++) {
            bucketList.add(new LinkedList<>());
        }

        //找出数据中最大值
        int max = arr[0];
        for (int i = 1; i < arr.length; i++) {
            if (max < arr[i]) {
                max = arr[i];
            }
        }

        //获取最大值的位数
        int maxRadix = (max + "").length();
        //从个位开始
        for (int i = 0; i < maxRadix; i++) {
            //将待排序元素放入桶中
            for (int j = 0; j < arr.length; j++) {
                //获取数字对应位上的值
                int radix = arr[j] / (int) Math.pow(10, i) % 10;
                //放入对应的桶中
                bucketList.get(radix).add(arr[j]);
            }
            //将桶中元素放回原数组
            int k = 0;
            for (int j = 0; j < 10; j++) {
                for (Integer number : bucketList.get(j)) {
                    arr[k++] = number;
                }
                bucketList.get(j).clear();
            }
        }
        return arr;
    }
}

 另:排序算法的稳定性

1. 稳定性的定义
假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,ri=rj,且ri在rj之前,而在排序后的序列中,ri仍在rj之前,则称这种排序算法是稳定的;否则称为不稳定的。

举个栗子:以{6,2,4,6,1}为例

a[0] a[1] a[2] a[3] a[4]
6 2 4 6 1

有两个6,a[0]和a[3]。排序结果就有两种可能:

1 2 4 6 6
原a[4] 原a[1] 原a[2] 原a[0] 原a[3]
原a[4] 原a[1] 原a[2] 原a[3] 原a[0]

如果排序结束后,a[0]可以保证一定在a[3]前头,也就是他们原有的顺序不变,那这种排序算法就是稳定的。反之,如果不能保证原有顺序,这种算法就是不稳定的

2. 常见算法的稳定性
堆排序、快速排序、希尔排序、直接选择排序不是稳定的排序算法,而基数排序、冒泡排序、直接插入排序、折半插入排序、归并排序是稳定的排序算法。

3. 稳定性的意义
1、如果只是简单的进行数字的排序,那么稳定性将毫无意义。
2、如果排序的内容仅仅是一个复杂对象的某一个数字属性,那么稳定性依旧将毫无意义(所谓的交换操作的开销已经算在算法的开销内了,如果嫌弃这种开销,不如换算法好了?)
3、如果要排序的内容是一个复杂对象的多个数字属性,但是其原本的初始顺序毫无意义,那么稳定性依旧将毫无意义。
4、除非要排序的内容是一个复杂对象的多个数字属性,且其原本的初始顺序存在意义,那么我们需要在二次排序的基础上保持原有排序的意义,才需要使用到稳定性的算法。例如要排序的内容是一组原本按照价格高低排序的对象,如今需要按照销量高低排序,使用稳定性算法,可以使得想同销量的对象依旧保持着价格高低的排序展现,只有销量不同的才会重新排序。(当然,如果需求不需要保持初始的排序意义,那么使用稳定性算法依旧将毫无意义)。

参考:

https://www.cnblogs.com/songjilong/p/12234856.html

https://blog.csdn.net/sunxianghuang/article/details/51872360

https://blog.csdn.net/it_zjyang/article/details/53406764

https://www.cnblogs.com/hydor/p/3530593.html

https://blog.csdn.net/chenliguan/article/details/53037482

posted @ 2022-02-05 18:50  zeroingToOne  阅读(109)  评论(0编辑  收藏  举报