排序算法总结


在这里插入图片描述

一、快速排序

public class QuickSort {
    public static void main(String[] args) {
        int[] arr = {6, 3, 5, 4, 9, 1, 7, 8, 2};
        quickSort(arr, 0, arr.length - 1);
        System.out.println(Arrays.toString(arr));
    }

    /**
     * 使数组在 l到 r上有序
     *
     * @param arr
     * @param l
     * @param r
     */
    public static void quickSort(int[] arr, int l, int r) {
        if (l < r) {
            // 如果加上下面这一行就是随机快排,从当前数组中随机选一个数和最后一个数交换,然后用这个数来作划分
            //swap(arr, l + (int) (Math.random() * (r - l + 1)), r);
            
            // p中固定只有两个元素,第一个表示等于区域开始位置,第二个表示等于区域结束位置
            int[] p = partition(arr, l, r);
            // 对小于区域进行排序
            quickSort(arr, l, p[0] - 1);
            // 对大于区域进行排序
            quickSort(arr, p[1] + 1, r);
        }
    }

    /**
     * 一次 partition过程,返回等于区域的起始下标
     *
     * @param arr
     * @param l   数组开始的下标
     * @param r   数组结束的下标
     * @return
     */
    public static int[] partition(int[] arr, int l, int r) {
        // 小于区域边界,最开始从 l-1开始
        int less = l - 1;
        // 大于区域边界,因为我们选取的就是最后一个元素,作为比较,所以不参与,从 r开始
        int more = r;
        // 用 l作为cur,当cur小于大于区域的边界
        while (l < more) {
            // 以最后一个元素为基准进行划分
            if (arr[l] < arr[r]) {
                // 小于区域的前一个数和当前数交换,比较下一个数
                swap(arr, ++less, l++);
            } else if (arr[l] > arr[r]) {
                // 大于区域的前一个数和当前数交换,继续比较当前数
                swap(arr, --more, l);
            } else {
                // 相等跳过
                l++;
            }
        }
        // 最后一个数没有参与排序,完成上面的比较后将最后一个数放到比较后的数组中,位置就是more的位置
        swap(arr, more, r);
        // 返回等于区域闭区间下标
        // less为小于区域最后一个数的下标,more为等于区域最后一个数的下标
        return new int[]{less + 1, more};
    }

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

在这里插入图片描述
当数组为 [1,2,3,4,5,6,7]时,经典快排选取最后一个数作为划分来比较其他的数,partition一遍的时候只搞定7这个数, 然后对剩下的[1,2,3,4,5,6]选择6进行partition又只搞定一个数(对比冒泡排序),对于n个这样的数,每一次partition都是O(n)的复杂度,所以这时整个复杂度就成了O(n^2)了。解决方法就是随机选取一个数和最后一个数交换,然后用最后一个数来进行划分,使得划分后大于区域和小于区域的数据量差不多,无论最好还是最坏情况它都是一个概率事件,在大样本环境下,经过概率学证明后可以得到随机快排时间复杂度可以达到O(N*logN)。

空间复杂度用于记录递归时每次划分位置的数,在最好情况下每次划分都在中间,空间复杂度O(logn),比如 [1,2,3,4,5,6,7,8],我们需要使用partition记录断点位置,断点如果都打在中间,则8条数据只需要记录3次断点即可,这为最好情况,但如果断点打在8,第二次打在7,第三次打在6…则8条数据,一共需要记录8次断点,这就为最差情况为O(n)。

  • 关于随机值的获取:取 [x, y] 区间整数:x + (int) (Math.random() * (y - x + 1))

二、归并排序

public class MergeSort {
    public static void main(String[] args) {
        int[] arr = {9, 3, 8, 1};
        sortProcess(arr, 0, arr.length - 1);
        System.out.println(Arrays.toString(arr));
    }

    /**
     * 排序过程
     *
     * @param arr
     * @param l   起始下标
     * @param r   结束下标
     */
    public static void sortProcess(int[] arr, int l, int r) {
        if (l == r) {
            return;
        }
        int mid = l + ((r - l) >> 1);
        // 左边归并排序
        sortProcess(arr, l, mid);
        // 右边归并排序
        sortProcess(arr, mid + 1, r);
        merge(arr, l, mid, r);
    }

    /**
     * 合并两个有序数组
     *
     * @param arr
     * @param l   起始小标
     * @param mid 中间位置(左边数组结束下标)
     * @param r   结束下标
     */
    public static void merge(int[] arr, int l, int mid, int r) {
        int[] help = new int[r - l + 1];
        int i = 0;
        // 指向左边数组起始位置
        int pLeft = l;
        // 指向右边数组起始位置
        int pRight = mid + 1;
        // 都没有到头,取出来比较
        while (pLeft <= mid && pRight <= r) {
            // 谁小谁填到 help数组中
            help[i++] = arr[pLeft] < arr[pRight] ? arr[pLeft++] : arr[pRight++];
        }
        // 右边的填完了 pRight超过 r
        while (pLeft <= mid) {
            help[i++] = arr[pLeft++];
        }
        // 左边的填完了 pLeft超过 mid
        while (pRight <= r) {
            help[i++] = arr[pRight++];
        }
        // 拷贝回原数组
        for (i = 0; i < help.length; i++) {
            arr[l + i] = help[i];
        }
    }
}
  • 时间复杂度:O(nlogn)
  • 空间复杂度:O(N),归并排序需要一个与原数组相同长度的数组做辅助来排序
  • 稳定性:归并排序是稳定的排序算法,help[i++] = arr[pLeft] < arr[pRight] ? arr[pLeft++] : arr[pRight++];这行代码可以保证当左右两部分的值相等的时候,先复制左边的值,这样可以保证值相等的时候两个元素的相对位置不变。
2.1 求数组的小和
/**
 * 小和问题
 *
 * [3, 1, 4, 6, 5, 2]
 * 3左边比3小的数:0
 * 1左边比1小的数:0
 * 4左边比4小的数:3, 1
 * 6左边比6小的数:3, 1, 4
 * 5左边比5小的数:3, 1, 4
 * 2左边比2小的数:1
 * 所以小和为:1*4 + 3*3 + 4*2 = 21
 *
 * 等价于求右边有多少个数,比当前数大
 * 3右边比3大的数有3个
 * 1右边比1大的数有4个
 * 4-------------2
 * 5-------------0
 * 2-------------0
 */
public class MergeSortSmallSum {
    public static void main(String[] args) {
        int[] arr = {3,1,4,6,5,2};
        System.out.println(mergeSort(arr, 0, arr.length - 1));
    }

    /**
     * 返回在l到r上产生的小和
     * @param arr
     * @param l
     * @param r
     * @return
     * (l+r)/2  l+(r-l)/2
     * a/2 = a >> 1
     */
    public static int mergeSort(int[] arr, int l, int r) {
        if (l == r) {
            return 0;
        }
        int mid = l + ((r - l) >> 1);
        // 左侧,右侧 产生的小和加上左右两侧整体产生的小和
        return mergeSort(arr, l, mid)
                + mergeSort(arr, mid + 1, r)
                + merge(arr, l, mid, r);
    }

    public static int merge(int[] arr, int l, int mid, int r) {
        int[] help = new int[r - l + 1];
        int i = 0;
        int p1 = l;
        int p2 = mid + 1;
        int res = 0;
        while (p1 <= mid && p2 <= r) {
            // 如果 p1 < p2,那么右边的数都比p1大,共有(r - p2 + 1)个
            res += arr[p1] < arr[p2] ? arr[p1] * (r - p2 + 1) : 0;
            help[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++];
        }
        while (p1 <= mid) {
            help[i++] = arr[p1++];
        }
        while (p2 <= r){
            help[i++] = arr[p2++];
        }
        for (i=0 ; i < help.length; i++) {
            arr[l+i] = help[i];
        }
        return res;
    }
}

在一次merge的过程中,我们可以求出左边数组中的每一个数对于右边数组来说,有多少个数比当前数大,例如[1, 3, 4][2, 5, 6]在merge的过程中,可以求出有3个比1大的数,有2个比3大的数,有2个比4大的数
在这里插入图片描述

2.1 求数组的逆序对
/**
 * @auther Mr.Liao
 * @date 2019/11/6 16:46
 * 在一个数组中,左边的数如果比右边的数大,则这两个数构成一个逆序对,请打印所有逆序对。
 * [9, 3, 8, 1]
 * 93 98 91 31 81
 */
public class MergeSort_nixudui {
    static int count = 0;

    public static void main(String[] args) {
        int[] arr = {4,5,6,7};
        sortProcess(arr, 0, arr.length - 1);
        System.out.println(Arrays.toString(arr));
        System.out.println(count);
    }

    public static void sortProcess(int[] arr, int l, int r) {
        if (l == r) {
            return;
        }
        int mid = (l + r) / 2;
        // 左边归并排序
        sortProcess(arr, l, mid);
        // 右边归并排序
        sortProcess(arr, mid + 1, r);
        merge(arr, l, mid, r);
    }

    public static void merge(int[] arr, int l, int mid, int r) {
        int[] help = new int[r - l + 1];
        int i = 0;
        int p1 = l;
        int p2 = mid + 1;
        // 都没有到头,取出来比较
        while (p1 <= mid && p2 <= r) {
            if (arr[p1] > arr[p2]) {
                help[i++] = arr[p2++];
                // 因为如果arr[p1]此时比右数组的当前元素arr[p2]大,
                // 那么左数组中arr[p1]后面的元素就都比arr[p2]大
                count += mid - p1 + 1;
            } else {
                help[i++] = arr[p1++];
            }
        }
        // 右边的到 r位置了
        while (p1 <= mid) {
            help[i++] = arr[p1++];
        }
        // 左边的到 mid位置了了
        while (p2 <= r) {
            help[i++] = arr[p2++];
        }
        for (i = 0; i < help.length; i++) {
            arr[l + i] = help[i];
        }
    }
}

三、堆排序

完全二叉树:完全二叉树从根结点到倒数第二层满足完美二叉树,最后一层可以不完全填充,其叶子结点都靠左对齐,堆就是完全二叉树,数组——完全二叉树(通过下标来维护彼此的关系)
在这里插入图片描述
大根堆:当前树(子树)中(包含子树)的最大值都是头部
小根堆:当前树(子树)中(包含子树)的最小值都是头部
数组——>大根堆
在这里插入图片描述
在这里插入图片描述

public class HeapSort {
    public static void main(String[] args) {
        int[] arr = new int[100];
        for (int i = 0; i < 100; i++) {
            arr[i] = (int)(Math.random() * 100);
        }
        heapSort(arr);
        System.out.println(Arrays.toString(arr));
    }

    public static void heapSort(int[] arr) {
        if (arr == null || arr.length < 2) {
            return;
        }
        // 形成大根堆 复杂度:O(n)
        for (int i = 0; i < arr.length; i++) {
            // 0 ~ i 之间形成大根堆
            heapInsert(arr, i);
        }
        // 大根堆中节点的个数
        int heapSize = arr.length;
        // 堆顶位置的数和最后一个数交换,最大值来到了数组的最后,堆大小减 1,排除原来堆顶这个数
        swap(arr, 0, --heapSize);
        while (heapSize > 0) {
            // 交换完之后,调整当前堆顶元素的位置,从新形成一个大根堆,继续这个过程
            heapify(arr, 0, heapSize);
            // 堆顶位置的数和最后一个数交换,最大值来到了数组的最后,堆大小减 1,排除原来堆顶这个数
            swap(arr, 0, --heapSize);
        }
    }

    /**
     * heapSize:形成的大根堆的数组元素的数量
     * 0 ~ heapSize-1下标位置中 index位置节点的值变小往下沉的过程
     * 下降的过程就是 index位置元素和它的左右子节点中较大的交换位置
     * @param arr
     * @param index
     * @param heapSize 当前大根堆的节点数 <= arr.length
     */
    public static void heapify(int[] arr, int index, int heapSize) {
        // index位置所在节点的左子节点位置
        int left = index * 2 + 1;
        // 左子节点不越界,也在堆上时
        while (left < heapSize) {
            // (left+1)为右子节点的下标,如果右子节点不越界 && 左子节点的值 < 右子节点的值
            // largest就表示两个子节点中值较大节点在数组中的下标
            int largest = left + 1 < heapSize && arr[left] < arr[left + 1] ? left + 1 : left;
            // 当前节点的值和较大子节点的值比较,返回较大节点值的下标
            largest = arr[index] > arr[largest] ? index : largest;
            // 如果是本身较大,则不用下沉,此位置就是最终位置
            if (largest == index) {
                break;
            }
            // 较大的不是本身,则和较大的节点交换位置
            swap(arr, index, largest);
            // 修改当前下标为较大节点的下标
            index = largest;
            // 找当前下标位置对应的左子节点下标
            left = 2 * index + 1;
        }
    }

    /**
     * 将数组index位置的元素加入到大根堆中
     * @param arr
     * @param index 数组中当前元素的下标
     */
    public static void heapInsert(int[] arr, int index) {
        // 如果当前节点大于父节点,交换位置,直到不大于父节点
        // 当 index跳到 0位置后,(0-1)/2=0,0位置和 0位置相等,跳出循环
        while (arr[index] > arr[(index - 1) / 2]) {
            // 当前节点和父节点交换
            swap(arr, index, (index - 1) / 2);
            // 当前节点跑到父节点的位置继续比较(和交换后的父节点比较)
            index = (index - 1) / 2;
        }
    }

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

堆的简单应用,求不断加入的数据整体的中位数
在这里插入图片描述

四、直接插入排序

public static void insertSort1(int[] arr) {
        // 从第二个位置取出每一个数去排序
        for (int i = 1; i < arr.length; i++) {
            // 从当前数的前一个数开始比较(有序区)
            // 如果当前位置的数小于有序区的最后一个数,则交换位置,直到大于等于有序区的某个数,或者比到头,自己最小
            for (int j = i - 1; j >= 0; j--) {
                if (arr[j + 1] < arr[j]) swap(arr, j, j + 1);
            }
        }
    }

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

五、直接选择排序

思路:假设第一个最小,从第二个遍历剩下的,遍历到一个比一下,遇到更小的则更新最小的下标,记录最小的下标,遍历完后和第一个位置的数交换,继续以同样的方式搞定除第一个外剩下的数组

public static void selectSort(int[] arr) {
    for (int i = 0; i < arr.length - 1; i++) {
        // 假设第i个数为数组中的最小值
        int minIndex = i;
        int min = arr[minIndex];
        // 从 i后面的数开始循环比较,遇到比 min更小的就赋值给 min
        for (int j = i + 1; j < arr.length; j++) {
            // 如果碰到比第一个小的就假设这个数为最小
            if (arr[j] < min) {
                min = arr[j];
                minIndex = j;
            }
        }
        // 循环结束找到数组中的最小值,与第一个值进行交换
        if (minIndex != i) {
            arr[minIndex] = arr[i];
            arr[i] = min;
        }
    }
}

六、希尔排序

public class ShellSort {
    /**
     * 交换法希尔排序思路
     */
    @Test
    public void sortProcess() {
        int[] arr = {8, 9, 1, 7, 2, 3, 5, 4, 6, 0};
        int temp;
        // 第一轮排序,10/2=5,将10个数据分成5组,每组两个元素,同一组的第二个元素在第一个元素后面第五个位置
        // 8-3,9-5,1-4,7-6,2-0 则在原顺序中 8-3 9-5 7-6 2-0 需要交换位置
        // 因为是两组,所以从下标 5位置开始遍历,即遍历 3,4,5,6,0
        // 遍历到的元素分别和组里的元素比较,比的时候使用直接插入的方式
        for (int i = 5; i < arr.length; i++) {
            //从下标5位置开始,进行插入排序
            for (int j = i - 5; j >= 0; j = j - 5) {
                //如果当前位置的数小于对应组里的数,就交换位置,然后继续比,直到比到头
                if (arr[j + 5] < arr[j]) {
                    Utils.swap(arr, j, j + 5);
                }
            }
        }
        // [3, 5, 1, 6, 0, 8, 9, 4, 7, 2]
        System.out.println("第一轮排序:" + Arrays.toString(arr));

        // 第二轮排序,继续分组5/2 = 2组 31097 56842
        // 从1开始遍历,遍历到就和当前组的元素比较
        for (int i = 2; i < arr.length; i++) {
            // 从第下标2开始,进行插入排序,不是从前一个位置向前比,而是从前2个位置向前比
            for (int j = i - 2; j >= 0; j = j - 2) {
                // 当前位置的数小于对应组里的元素,则交换位置继续比(0和1比,比完后和3比)
                if (arr[j + 2] < arr[j]) {
                    Utils.swap(arr, j, j + 2);
                }
            }
        }
        // [0, 2, 1, 4, 3, 5, 7, 6, 9, 8]
        System.out.println("第二轮排序:" + Arrays.toString(arr));

        // 第二轮排序,继续分组2/2 = 1组,此时的过程就是直接插入排序
        // 但是这时0已经在前面了,杜绝了直接插入排序,0在最后要比很多次的情况
        for (int i = 1; i < arr.length; i++) {
            for (int j = i - 1; j >= 0; j = j - 1) {
                if (arr[j] > arr[j + 1]) {
                    Utils.swap(arr, j, j + 1);
                }
            }
        }
        // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
        System.out.println("第三轮排序:" + Arrays.toString(arr));
    }


    /**
     * 交换法
     *
     * @param arr
     */
    public static void shellSortSwap(int[] arr) {
        // 10个元素,第一次5组,第二次2组,第三次1组 gap=5,2,1
        for (int gap = arr.length / 2; gap > 0; gap = gap / 2) {
            // 从gap位置开始遍历
            for (int i = gap; i < arr.length; i++) {
                // 直接插入排序的方式进行排序,从后往前比,步长为gap
                for (int j = i - gap; j >= 0; j = j - gap) {
                    // 如果前一个位置元素大于后一个位置元素,则交换位置
                    if (arr[j] > arr[j + gap]) {
                        Utils.swap(arr, j, j + gap);
                    }
                }
            }
        }
    }

    @Test
    public void shellSortSwap() {
        int[] arr = {8, 9, 1, 7, 2, 3, 5, 4, 6, 0};
        shellSortSwap(arr);
        System.out.println(Arrays.toString(arr));
    }

    /**
     * 移位法,效率更高,速度更快
     *
     * @param arr
     */
    public static void shellSortOffset(int[] arr) {
        // 10个元素,第一次5组,第二次2组,第三次1组 gap=5,2,1
        for (int gap = arr.length / 2; gap > 0; gap = gap / 2) {
            // 从gap位置开始遍历
            for (int i = gap; i < arr.length; i++) {
                // 当前位置
                int j = i;
                // 当前值
                int temp = arr[j];
                // 如果当前位置的数小于前面的数
                if (arr[j] < arr[j - gap]) {
                    // 如果没有比到头(j - gap >=o),并且当前值小于前面的值
                    while (j - gap >= 0 && temp < arr[j - gap]) {
                        // 当前位置的数替换为前面较大的数
                        arr[j] = arr[j - gap];
                        // 当前位置移动到前面位置
                        j = j - gap;
                    }
                    // 此时j在前面位置,前面位置的值替换为当前位置的值
                    arr[j] = temp;
                }
            }
        }
    }

    @Test
    public void shellSortOffset() {
        int[] arr = {8, 9, 1, 7, 2, 3, 5, 4, 6, 0};
        shellSortOffset(arr);
        System.out.println(Arrays.toString(arr));
    }
}
































评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值