【LeetCode】912. 排序数组(中等)

912. 排序数组

给你一个整数数组 nums,请你将该数组升序排列。

示例 1:

输入:nums = [5,2,3,1]
输出:[1,2,3,5]

示例 2:

输入:nums = [5,1,1,2,0,0]
输出:[0,0,1,1,2,5]

提示:

1 <= nums.length <= 5 * 104
-5 * 104 <= nums[i] <= 5 * 104

目录

 01-问题分析

 02-时间复杂度为O(n²)的排序算法

1. 选择排序

2. 冒泡排序

3. 插入排序

03-时间复杂度为O(nlogn)的排序算法

1. 快速排序

2. 归并排序

3. 堆排序

04-桶排序

1. 计数排序

2. 基数排序


01-问题分析

  • 本题考察常用排序算法的使用
  • 常用排序算法
    • O(n²)
      • 选择排序
      • 冒泡排序
      • 插入排序
    • O(nlogn)
      • 快速排序
      • 归并排序
      • 堆排序
    • 桶排序
      • 计数排序
      • 基数排序
  •  本题目要求排序算法时间复杂度为 O(nlogn)

02-时间复杂度为O(n²)的排序算法

注:以下3种排序算法时间复杂度较高,无法通过本题

1. 选择排序

  • 排序思路
    • 每次遍历选择出数组中最小的元素,放到数组待排序序列的左边
    • 每次遍历结束后待排序序列中最小的元素在序列最左边,排序整个数组需要遍历n-1次
  • 算法分析
    • 时间复杂度:O(n²)
      • 数组一共遍历n-1次,每次遍历n-i个元素,时间复杂度为O(n²)
    • 空间复杂度:O(1)
      • 选择排序算法使用有限个额外变量完成数组排序,空间复杂度为O(1)
    • 稳定性
      • 选择排序不具有稳定性
      • 最小元素和待排序元素最左边的元素交换时,有可能打乱原数组中值相同元素的先后顺序
      • 如 {2,2,1},第1次遍历,将1和最左边的2交换,2的顺序被打乱
class Solution {
    public void selectionSort(int[] arr) {
        if (arr == null || arr.length < 2) {
            return;
        }

        for (int i = 0; i < arr.length - 1; i++) {// i ~ N-1
            int minIndex = i;
            for (int j = i; j < arr.length; j++) {// i ~ N-1 上找最小值的下标
                minIndex = arr[j] < arr[minIndex] ? j : minIndex;
            }
            swap(arr, i, minIndex);
        }
    }

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

    public int[] sortArray(int[] nums) {
        selectionSort(nums);
        return nums;
    }
}

2. 冒泡排序

  • 排序思路
    • 每次遍历将当前元素和它右边的元素进行比较,谁大谁把谁放到右边
    • 每次遍历结束后待排序序列最大的元素都会在序列最右边,排序整个数组需要遍历n-1次
  • 算法分析
    • 时间复杂度:O(n²)
      • 数组一共遍历n-1次,每次遍历n-i个元素,时间复杂度为O(n²)
    • 空间复杂度
      • 冒泡排序算法使用有限个额外变量完成数组排序,空间复杂度为O(1)
    • 稳定性:稳定
      • 冒泡排序具有稳定性
      • 冒泡排序每次把当前元素和它右边的元素比较,谁大谁放右边,交换发生在相邻的两个元素间,不会改变原数组值相同的元素的先后顺序
class Solution {
    public void bubbleSort(int[] arr) {
        if (arr == null || arr.length < 2) {
            return;
        }

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

    // i和j是一个位置的话,会出错
    public void swap(int[] arr, int i, int j) {
        arr[i] = arr[i] ^ arr[j];
        arr[j] = arr[i] ^ arr[j];
        arr[i] = arr[i] ^ arr[j];
    }

    public int[] sortArray(int[] nums) {
        bubbleSort(nums);
        return nums;
    }
}

3. 插入排序

  • 排序思路
    • 每次遍历将当前元素插入到有序序列中,初始有序序列长度为1
    • 每次遍历结束后,有序序列的长度会加1,排序整个数组需要遍历n-1次
  • 算法分析
    • 时间复杂度:O(n²)
      • i从1开始,数组一共遍历n-1次,每次最多遍历n-i个元素,时间复杂度为O(n²)
    • 空间复杂度
      • 插入排序算法使用有限个额外变量完成数组排序,空间复杂度为O(1)
    • 稳定性:稳定
      • 插入排序具有稳定性
      • 插入排序从左到右遍历数组,每次把当前元素插入到有序序列中,不会改变原数组值相同的元素的先后顺序
class Solution {
    public void insertionSort(int[] arr) {
        if (arr == null || arr.length < 2) {
            return;
        }

        // 0~0 有序的
        // 0~i 想有序
        for (int i = 1; i < arr.length; i++) {// 0~i 做到有序
            for (int j = i - 1; j >= 0 && arr[j] > arr[j + 1]; j--) {
                swap(arr, j , j + 1);
            }
        }
    }

    // i和j是一个位置的话,会出错
    public void swap(int[] arr, int i, int j) {
        arr[i] = arr[i] ^ arr[j];
        arr[j] = arr[i] ^ arr[j];
        arr[i] = arr[i] ^ arr[j];
    }

    public int[] sortArray(int[] nums) {
        insertionSort(nums);
        return nums;
    }
}

03-时间复杂度为O(nlogn)的排序算法

1. 快速排序

  • 排序思路
    • 在数组中随机选择一个数字作为划分值,对数组进行partition过程
    • 对划分值的左右两部分递归的调用这个过程
  • partition操作(荷兰国旗问题)
    • 一个处理一个数组在L~R上元素的方法,默认以最右边(也可以是最左边)的元素作为划分。返回一个两个元素的int型数组,数组元素为等于区域的左边界和右边界
    • 在L~R上从左到右遍历当前数组,将数组在L~R上的部分划分成3个区域。把小于划分值元素都放到划分值的左边,大于划分值的元素都放到划分值的右边,等于划分值的数字放中间。返回等于区域的边界
  • 算法分析
    • 时间复杂度:O(nlogn)
      • partition操作的时间复杂度为O(n)
      • 最好情况下,快速排序需要做O(logn)次划分,时间复杂度为O(nlogn)
      • 最坏情况下,每次划分只能减少1个元素,时间复杂度为O(n²)
      • 平均情况下,时间复杂度为O(nlogn)
    • 空间复杂度:O(n)
      • partition操作额外空间复杂度为O(1)
      • 最坏情况下,空间复杂度为O(n)
    • 稳定性
      • 快速排序不具有稳定性
      • partition操作有可能打乱原数组中值相同元素的先后顺序
class Solution {
    public void quickSort(int[] arr) {
        if (arr == null || arr.length < 2) {
            return;
        }
        process(arr, 0, arr.length - 1);
    }

    // 将arr[L..R]排好序
    public void process(int[] arr, int L, int R) {
        if (L < R) {
            swap(arr, L + (int)(Math.random() * (R - L + 1)), R); // 要想通过本题,需要使用随机值拆分数组
            int[] p = partition(arr, L, R);
            process(arr, L, p[0] - 1); // 小于区域
            process(arr, p[1] + 1, R);// 大于区域
        }
    }

    // 这是一个处理arr[L..R]的函数
    // 默认以arr[R]做划分    arr[R] -> p
    // 返回一个两个元素的int数组,为等于区域的左边界和右边界
    public int[] partition(int[] arr, int L, int R) {
        int lt = L;// 小于区域的后一个位置
        int gt = R - 1;// 大于区域的前一个位置

        while (L <= gt) {// L为当前位置    arr[R]为划分值
            if (arr[L] < arr[R]) {
                // 当前数比划分值小,交换当前数和小于区域后一个数。当前位置后移,小于区域后移。
                swap(arr, lt++, L++);
            } else if (arr[L] > arr[R]) {
                // 当前数比划分值大,交换当前数和大于区域前一个数。当前位置不动,大于区域前移。
                swap(arr, gt--, L);
            } else {
                // 当前数和划分值相等,当前位置后移,同时等于区域后移。
                L++;
            }
        }

        swap(arr, gt + 1, R);
        return new int[]{lt, gt + 1};
    }

    public void swap(int[] arr, int i, int j) {
        if (i == j) {
            return;
        }
        arr[j] ^= arr[i] ^= arr[j];
        arr[i] ^= arr[j];
    }

    public int[] sortArray(int[] nums) {
        quickSort(nums);
        return nums;
    }
}

2. 归并排序

  • 排序思路
    • 把数组从中间位置划分成两部分,对这两部分递归的进行归并排序(这两部分已然有序),最后通过merge操作将这两部分合并成一个完整的数组
    • 当划分后的数组长度为1时,此时这个数组显然是有序的
  • merge操作
    • 新建一个新的数组存放两个数组(这两个数组已然有序)合并后的结果
    • 分别遍历这俩个数组,将当前数组元素相对较小的元素放到新数组中
    • 将新数组的元素拷贝回原数组对应位置
  • 算法分析
    • 时间复杂度:O(nlgn)
      • merge操作的时间复杂度为O(n)
      • 归并排序需要进行log(n)次划分,时间复杂度为O(nlogn)
    • 空间复杂度:O(n)
      • 归并排序的递归深度为logn,每层的merge操作需要O(n)的额外空间。但同一时间merge操作只需要O(n)的额外空间(进行上层的merge操作时,下层merge操作申请的新数组已经释放掉了),所以归并排序的空间复杂度是O(n)
    • 稳定性
      • 归并排序具有稳定性
      • merge操作遍历两个子数组的顺序是从左往右,将较小的元素放到新数组时也不会改变原数组值相同的元素的先后顺序
class Solution {
    public void mergeSort(int[] arr) {
        if (arr == null || arr.length < 2) {
            return;
        }
        process(arr, 0, arr.length - 1);
    }

    // 请把arr[L..R]排有序
    // l...r N
    // T(N) = 2 * T(N / 2) + O(N)
    public void process(int[] arr, int L, int R) {
        if (L == R) {
            return;
        }
        int mid = L + ((R - L) >> 1);
        process(arr, L, mid);
        process(arr, mid + 1, R);
        merge(arr, L, mid, R);
    }

    public static void merge(int[] arr, int L, int M, int R) {
        int[] help = new int[R - L + 1];
        int i = 0;
        int p1 = L;
        int p2 = M + 1;

        while (p1 <= M && p2 <= R) {
            help[i++] = arr[p1] <= arr[p2] ? arr[p1++] : arr[p2++];
        }

        // 要么p1越界了,要么p2越界了
        while (p1 <= M) {
            help[i++] = arr[p1++];
        }
        while (p2 <= R) {
            help[i++] = arr[p2++];
        }

        for (i = 0; i < help.length; i++) {
            arr[L + i] = help[i];
        }
    }

    public int[] sortArray(int[] nums) {
        mergeSort(nums);
        return nums;
    }
}

3. 堆排序

  • 排序思路
    • 堆:是一种完全二叉树,某个节点的值总是不大于(大根堆)或不小于(小根堆)其父节点的值。即对于堆中的某个节点,其值大于(或小于)等于左右子树中的所有节点的值。(以下所说的堆都是用数组表示,完全二叉树 == 数组)
    • 完全二叉树的性质(用数组表示完全二叉树,i为数组元素的下标)
      • 设一个节点下标为i,则左孩子下标为i * 2 + 1,右孩子下标为i * 2 + 2
      • 设一个节点下标为i,则其父节点下标为(i - 1) / 2
    • 堆排序:利用堆的特点,对数组进行排序
      • 对待排序数组的每个元素进行heapInsert操作,将数组转化成大根堆。此时数组的第一个元素为大根堆的根节点,同时也是数组的最大值
      • 将大根堆的第一个元素和最后一个元素交换,此时数组的最后一个元素为最大值。将大根堆的长度减1,对第一个元素进行heapify操作,此时除了最后一个元素,数组还是大根堆。重复这个过程,直到大根堆的长度为
  • heapInsert操作
    • heapInsert操作:给定一个节点的下标为index,按照大根堆的特性,将其向上移动
    • 判断当前节点的值是否大于父节点(如果有父节点)的值,如果大于,交换这两个节点的值,相当于这个节点向上移动。重复这个过程,直到当前节点无法向上移动或成为根节点
  • heapify操作
    • heapify操作:给定一个节点的下标为index,按照大根堆的特性,将其向下移动
    • 判断当前节点的值是否小于左孩子或右孩子(如果有左孩子和右孩子)的值,如果小于,则将其和左右孩子中值较大的孩子交换,相当于将这个节点向下移动。重复这个过程,直到当前节点无法向下移动或成为叶节点
  • 算法优化
    • 构造最大堆的过程使用heapify代替heapInsert,从完全二叉树的最后一个非叶子节点(下标为(arr.length - 1) / 2)开始,直到根节点的所有节点,进行heapify操作,也可以将数组转化成大根堆
    • 减少堆排序第一个过程的时间复杂度:O(nlogn) =》O(n)
  • 算法分析
    • 时间复杂度:O(nlogn)
      • heapInsert操作,时间复杂度为O(logn)
      • heapify操作,时间复杂度为O(logn)
      • 堆排序需要对所有数组元素进行heapInsert和heapify操作,时间复杂度为O(nlogn)
    • 空间复杂度:O(1)
      • 堆排序算法使用有限个额外变量完成数组排序,空间复杂度为O(1)
    • 稳定性
      • 堆排序不具有稳定性
class Solution {
    public void heapSort(int[] arr) {
        if (arr == null || arr.length < 2) {
            return;
        }

        /*//顺序插入每个元素,将数组转化成大根堆 O(NlogN)
        for (int i = 0; i < arr.length; i++) {//O(N)
            heapInert(arr, i);//O(logN)
        }*/

        //让整个数组直接变成大根堆 O(N)
        for (int i = arr.length - 1; i >= 0; i--) {
            heapify(arr, i, arr.length);
        }

        int heapSize = arr.length;
        swap(arr, 0, --heapSize);

        while (heapSize > 0) {//O(N)
            heapify(arr, 0, heapSize);//O(logN)
            swap(arr, 0, --heapSize);//O(1)
        }
    }

    //某个数现在处在index位置,向上继续移动
    public void heapInert(int[] arr, int index) {
        while (arr[index] > arr[(index - 1) / 2]) {
            swap(arr, index, (index - 1) / 2);
            index = (index - 1) / 2;
        }
    }

    //某个数在index位置,能否往下移动
    public void heapify(int[] arr, int index, int heapSize) {
        int left = index * 2 + 1;//左孩子的下标
        while (left < heapSize) {
            //两个孩子中,谁的值大,把下标给largest
            int largest = left + 1 < heapSize && arr[left + 1] > arr[left] ? left + 1 : left;

            //父和较大的孩子之间,谁的值大,把下标给largest
            largest = arr[largest] > arr[index] ? largest : index;
            if (largest == index) {
                break;
            }
            swap(arr, index, largest);
            index = largest;
            left = index * 2 + 1;
        }
    }

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

    public int[] sortArray(int[] nums) {
        heapSort(nums);
        return nums;
    }
}

04-桶排序

1. 计数排序

  •  排序思路
    • 用一个长度等同于数组最大元素值加一的数组,存储数组中每个元素出现的次数,然后通过这个数组构造新数组,完成排序
    • 适用于数组元素值差距较小的数组
  • 算法分析
    • 时间复杂度 O(n)
    • 空间复杂度 O(n)
    • 稳定性
      • 遍历数组的顺序从前到后,排序后原数组值相同元素的先后顺序
class Solution {
    public static void countSort(int[] arr) {
		if (arr == null || arr.length < 2) {
			return;
		}
		int max = Integer.MIN_VALUE;
		for (int i = 0; i < arr.length; i++) {
			max = Math.max(max, Math.abs(arr[i]));
		}
		int[][] bucket = new int[max + 1][2];
		for (int i = 0; i < arr.length; i++) {
			if (arr[i] >= 0) {
				bucket[arr[i]][0]++;
			} else {
				bucket[-arr[i]][1]++;
			}
		}
		int i = 0;
		for (int j = bucket.length - 1; j > 0; j--) {
			while (bucket[j][1]-- > 0) {
				arr[i++] = -j;
			}
		}
		for (int j = 0; j < bucket.length; j++) {
			while (bucket[j][0]-- > 0) {
				arr[i++] = j;
			}
		}
	}

    public int[] sortArray(int[] nums) {
        countSort(nums);
        return nums;
    }
}

2. 基数排序

  • 排序思路
    • 对数组从低位开始,根据每一位的数字进行排序
    • 每一位数字的排序,都新建一个count数组用于记录当前某一位大于等于count数组下标的元素出现了几次,同时对数字按这一位的大小进行分区,然后倒序把原数组元素按分区放入桶中,最后把桶中的元素复制回原数组,完成某一位的排序
    • 最终数组将会有序
  • 算法分析
    • 稳定性
      • 倒序把数组元素倒入桶中,在从桶中取出。这样就实现了类似于栈的功能,先入后出,原数组值相同元素的先后顺序不会改变
      • 基数排序具有稳定性
class Solution {
    //基数排序
    //范围:-50000-50000
    public void radixSort(int[] arr) {
        if (arr == null || arr.length < 2) {
            return;
        }
        for (int i = 0; i < arr.length; i++) {
            arr[i] += 50000;
        }
        radixSort(arr, 0, arr.length - 1, maxbits(arr));
    }

    //获取数组中最大数字的位数
    public int maxbits(int[] arr) {
        int max = Integer.MIN_VALUE;
        for (int num : arr) {
            max = Math.max(num, max);
        }
        int res = 0;
        while (max != 0) {
            res++;
            max /= 10;
        }
        return res;
    }

    //获取某个数的某位置的数字
    public int getDigit(int x, int d) {
        return x / (int)Math.pow(10, d - 1) % 10;
    }

    //对一个最大位数为digit的数组,在begin到end上进行基数排序
    public void radixSort(int[] arr, int begin, int end, int digit) {
        final int radix = 10;
        int i = 0, j = 0;

        int[] bucket = new int[end - begin + 1];//桶的长度为待排序数字元素个数

        for (int d = 1; d <= digit; d++) {
            int[] count = new int[radix];

            //使用count数组统计原数组每个元素某一位数字的出现次数
            for (i = begin; i <= end; i++) {
                j = getDigit(arr[i], d);
                count[j]++;
            }

            //count数组下标为i的元素值对应原数组中当前位大于等于i的数字出现的次数
            //相当于对桶进行了分区,0-count[0]下标的区域为0区域,count[0]-count[1]为1区域,... ,count[8]-count[9]为9区域
            for (i = 1; i < radix; i++) {
                count[i] += count[i - 1];
            }

            //将原数组的每个元素倒序倒入桶中对应分区
            for (i = end; i >= begin; i--) {
                j = getDigit(arr[i], d);
                bucket[count[j]-- - 1] = arr[i];
            }

            //将桶中元素拷贝回原数组
            for (i = begin, j = 0; i <= end; i++, j++) {
                arr[i] = bucket[j];
            }
        }
    }

    public int[] sortArray(int[] nums) {
        radixSort(nums);
        if (nums == null || nums.length < 2) {
            return nums;
        }
        for (int i = 0; i < nums.length; i++) {
            nums[i] -= 50000;
        }
        return nums;
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

晴雪月乔

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值