给你一个整数数组 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:选择排序(算法正确但不高效,会超时)
思路:每一轮选取未排定的部分中最小的那个元素交换到未排定部分的最开头,经过若干个步骤,就能排定整个数组。即:先选出最小的,再选出第二小的,以此类推。
class Solution {
public int[] sortArray(int[] nums) {
for (int i = 0; i < nums.length - 1; i++) {
int minIndex = i;
for (int j = i + 1; j < nums.length; j++) {
if (nums[j] < nums[minIndex]) {
minIndex = j;
}
}
swap(nums, i, minIndex);
}
return nums;
}
private void swap(int[] nums, int index1, int index2) {
int t = nums[index1];
nums[index1] = nums[index2];
nums[index2] = t;
}
}
复杂度分析
时间复杂度:O(N^2),N是 nums 的长度。
空间复杂度:O(1)。
解法2:冒泡排序 (算法正确但不高效,会超时)
基本思想
-
基本思想:外层循环每一次经过两两比较,把每一轮未排定部分最大的元素放到了数组的末尾;
-
依次将相邻的两个元素进行比较,把较大的元素交换到后面,这样一轮循环下来,就可以找到这一轮循环中最大的那个元素,我们把这个过程形象地称之为“冒泡”;
-
由于每一轮循环都「冒泡」出一个这一轮循环最大的元素,所以上一轮循环的最后一个元素,没有必要再参加下一轮循环的比较了;
-
「冒泡排序」有个特点:在遍历的过程中,提前检测到数组是有序的,从而结束排序,而不像「选择排序」那样,即使输入数据是有序的,「选择排序」依然需要很「死板地」地走完所有的流程。
class Solution {
public int[] sortArray(int[] nums) {
for (int i = nums.length - 1; i >= 0; i--) {
boolean sorted = true;
// 先默认数组是有序的,只要发生一次交换,就必须进行下一轮比较
// 如果在内层循环中,都没有执行一次交换操作,说明此时数组已经是升序数组
for (int j = 0; j < i; j++) {
if (nums[j] > nums[j + 1]) {
swap(nums, j, j + 1);
sorted = false;
}
}
if (sorted == true) {
break;
}
}
return nums;
}
private void swap(int[] nums, int i, int j) {
int t = nums[i];
nums[i] = nums[j];
nums[j] = t;
}
}
复杂度分析:
时间复杂度:O(N^2),这里 N是数组的长度;
空间复杂度:O(1),使用到常数个临时变量。
解法3:插入排序
思路:每次将一个数字插入一个有序的数组里,成为一个长度更长的有序数组,有限次操作以后,数组整体有序。
结论:稳定排序,在接近有序的情况下,表现优异。
特点:「插入排序」可以提前终止内层循环(体现在 nums[j - 1] > temp
不满足时),在数组「几乎有序」的前提下,「插入排序」的时间复杂度可以达到 O(N);
class Solution {
public int[] sortArray(int[] nums) {
for (int i = 1; i < nums.length; i++) {
int temp = nums[i];
int j = i;
while (j > 0 && nums[j - 1] > temp) {
nums[j] = nums[j -1];
j--;
}
nums[j] = temp;;
}
return nums;
}
}
复杂度分析
时间复杂度:O(N^2),N是 nums 的长度。
空间复杂度:O(1)。
解法4:归并排序
基本思路:借助额外空间,合并两个有序数组,得到更长的有序数组。
归并排序是一种分治法的典型应用,它将待排序的数据分为两半,分别对这两半进行排序,然后将排序好的两半合并成一个有序的数组。
class Solution {
public int[] sortArray(int[] nums) {
mergeSort(nums, 0, nums.length - 1);
return nums;
}
private void mergeSort(int[] nums, int left, int right) {
if (left >= right) {
return;
}
int mid = left + (right - left) / 2;
mergeSort(nums, left, mid);
mergeSort(nums, mid + 1, right);
mergeTwoSortedArray(nums, left, mid, right);
}
// 合并两个有序数组 nums[left,...,mid] nums[mid + 1,...,right]
private void mergeTwoSortedArray(int[] nums, int left, int mid, int right) {
int length = right - left + 1;
int[] temp = new int[length];
// nums[left,...,mid] 对应下标 temp[0,...,mid- left]
// nums[mid + 1,...,right] 对应下标 temp[mid + 1 - left,...,right - left]
for (int i = 0; i < length; i++) {
temp[i] = nums[left + i];
}
// 合并两个有序数组,l 代表左数组的起始下标,r 代表右数组的起始下标
int l = 0;
int r = mid + 1 - left;
// temp[i] 对应 nums[i + left]
// l:[0,...,mid - left] r:[mid + 1 - left,...,right - left]
for (int i = 0; i < length; i++) {
if (l > mid - left) {
nums[left + i] = temp[r++];
} else if (r > right - left) {
nums[left + i] = temp[l++];
} else if (temp[l] <= temp[r]) {
nums[left + i] = temp[l++];
} else {
nums[left + i] = temp[r++];
}
}
}
}
另一种写法:
temp 用于合并两个有序数组的辅助数组,全局使用一份,避免多次创建和销毁
class Solution {
public int[] sortArray(int[] nums) {
int[] temp = new int[nums.length];
mergeSort(nums, 0, nums.length - 1, temp);
return nums;
}
private void mergeSort(int[] nums, int left, int right, int[] temp) {
if (left == right) {
return;
}
int mid = left + (right - left) / 2;
mergeSort(nums, left, mid, temp);
mergeSort(nums, mid + 1, right, temp);
// nums[left,..., mid] 和 nums[mid + 1,..., right] 已经是两个 分别有序 的数组
// 如果nums[mid] <= nums[mid + 1],则nums[left,..., right]本身有序,无需合并
if (nums[mid] <= nums[mid + 1]) {
return;
}
mergeTwoSortedArray(nums, left, mid, right, temp);
}
private void mergeTwoSortedArray(int[] nums, int left, int mid, int right, int[] temp) {
for (int i = left; i <= right; i++) {
temp[i] = nums[i];
}
int l = left;
int r = mid + 1;
for (int i = left; i <= right; i++) {
if (l > mid) {
nums[i] = temp[r++];
} else if (r > right) {
nums[i] = temp[l++];
} else if (temp[l] <= temp[r]) {
nums[i] = temp[l++];
} else {
nums[i] = temp[r++];
}
}
}
}
复杂度分析
时间复杂度:O(n logn),n是 nums 的长度。
空间复杂度:O(n)。
解法5:快速排序
快速排序 1:基本快速排序(算法正确但不高效,会超时)
class Solution {
// 快速排序 1:基本快速排序
Random random = new Random();
public int[] sortArray(int[] nums) {
if (nums.length == 0) {
return nums;
}
randomizedQuickSort(nums, 0, nums.length - 1);
return nums;
}
private void randomizedQuickSort(int[] nums, int left, int right) {
if (left < right) {
int pos = randomizedPartition(nums, left, right);
randomizedQuickSort(nums, left, pos - 1);
randomizedQuickSort(nums, pos + 1, right);
}
}
private int randomizedPartition(int[] nums, int left, int right) {
int randomIndex = random.nextInt(right - left + 1) + left;
swap(nums, randomIndex, left);
return partition(nums, left, right);
}
private int partition(int[] nums, int left, int right) {
int pivot = nums[left];
int lt = left;
// 循环不变量:
// all in [left + 1, lt] < pivot
// all in [lt + 1, right] >= pivot
for (int i = left + 1; i <= right; i++) {
if (nums[i] < pivot) {
lt++;
swap(nums, i, lt);
}
}
swap(nums, left, lt);
return lt;
}
private void swap(int[] nums, int i, int j) {
int t = nums[i];
nums[i] = nums[j];
nums[j] = t;
}
}
快速排序 2:双指针(指针对撞)快速排序
class Solution {
// 快速排序 2:双指针(指针对撞)快速排序
private static final Random RANDOM = new Random();
public int[] sortArray(int[] nums) {
if (nums.length == 0) {
return nums;
}
randomizedQuickSort(nums, 0, nums.length - 1);
return nums;
}
private void randomizedQuickSort(int[] nums, int left, int right) {
if (left < right) {
int pos = randomizedPartition(nums, left, right);
randomizedQuickSort(nums, left, pos - 1);
randomizedQuickSort(nums, pos + 1, right);
}
}
private int randomizedPartition(int[] nums, int left, int right) {
int randomIndex= RANDOM.nextInt(right - left + 1) + left;
swap(nums, left, randomIndex);
return partition(nums, left, right);
}
private int partition(int[] nums, int left, int right) {
int pivot = nums[left];
int lt = left + 1;
int gt = right;
// 循环不变量:
// all in [left + 1, lt) <= pivot
// all in (gt, right] >= pivot
while (true) {
while (lt <= right && nums[lt] < pivot) {
lt++;
}
while (gt > left && nums[gt] > pivot) {
gt--;
}
if (lt > gt) {
break;
}
swap(nums, lt, gt);
lt++;
gt--;
}
swap(nums, left, gt);
return gt;
}
private void swap(int[] nums, int i, int j) {
int t = nums[i];
nums[i] = nums[j];
nums[j] = t;
}
}
快速排序 3:三指针快速排序
class Solution {
// 快速排序 3:三指针快速排序
private static final Random RANDOM = new Random();
public int[] sortArray(int[] nums) {
if (nums.length == 0) {
return nums;
}
randomizedQuickSort(nums, 0, nums.length - 1);
return nums;
}
private void randomizedQuickSort(int[] nums, int left, int right) {
if (left < right) {
int pos = randomizedPartition(nums, left, right);
randomizedQuickSort(nums, left, pos - 1);
randomizedQuickSort(nums, pos + 1, right);
}
}
private int randomizedPartition(int[] nums, int left, int right) {
int randomIndex = RANDOM.nextInt(right - left + 1) + left;
swap(nums, left, randomIndex);
return partition(nums, left, right);
}
private int partition(int[] nums, int left, int right) {
int pivot = nums[left];
int lt = left;
int gt = right + 1;
int i = left + 1;
// 循环不变量:
// all in [left + 1, lt] < pivot
// all in [lt + 1, i) == pivot
// all in [gt, right] > pivot
while (i < gt) {
if (nums[i] < pivot) {
lt++;
swap(nums, i, lt);
i++;
} else if (nums[i] == pivot) {
i++;
} else {
gt--;
swap(nums, i, gt);
}
}
swap(nums, left, lt);
return lt;
}
private void swap(int[] nums, int i, int j) {
int t = nums[i];
nums[i] = nums[j];
nums[j] = t;
}
}
复杂度分析
时间复杂度:基于随机选取主元的快速排序时间复杂度为期望 O(nlogn),其中 n 为数组的长度。
空间复杂度:O(h),其中 h 为快速排序递归调用的层数。我们需要额外的 O(h) 的递归调用的栈空间,由于划分的结果不同导致了快速排序递归调用的层数也会不同,最坏情况下需 O(n) 的空间,最优情况下每次都平衡,此时整个递归树高度为 logn,空间复杂度为 O(logn)。
解法6:堆排序
思路和算法
堆排序的思想就是先将待排序的序列建成大根堆,使得每个父节点的元素大于等于它的子节点。此时整个序列最大值即为堆顶元素,我们将其与末尾元素交换,使末尾元素为最大值,然后再调整堆顶元素使得剩下的 n−1个元素仍为大根堆,再重复执行以上操作我们即能得到一个有序的序列。
以上思想可归纳为两个操作:
(1)根据初始数组去构造初始堆(构建一个完全二叉树,保证所有的父结点都比它的孩子结点数值大)。
(2)每次交换第一个和最后一个元素,输出最后一个元素(最大值),然后把剩下元素重新调整为大根堆。
arr.length / 2 - 1 或者 (arr.length - 1) / 2 表示第一个非叶子节点:
首先一个n个节点二叉树的度n-1,从下往上看,因为除了根节点以为每个节点都有一个入度
设n个节点中 有x个非叶子节点和y个叶子节点,x+y =n ,从上往下看,所有的非叶子节点都有两个出度,叶子节点没有出度,所以 2x = n-1 = x+y-1,即 x=y-1
从上面式子可知道 非叶子节点比叶子节点少一个,而 int 型在在进行除法时会自动去除小数点,所以arr.leng /2 就类似于(n-1)/2 而这是数组下标,都需要减一,所以int i = arr.length/2 -1 表示第一个非叶子节点。
class Solution {
public int[] sortArray(int[] nums) {
heapSort(nums);
return nums;
}
private void heapSort(int[] nums) {
int len = nums.length - 1;
buildMaxHeap(nums, len);
for (int i = len; i >= 1; i--) {
swap(nums, 0, i);
len--;
maxHeapify(nums, 0, len);
}
}
// 初始化
private void buildMaxHeap(int[] nums, int len) {
// 从第一个非叶子结点从下至上,从右至左调整结构
for (int i = len / 2; i >= 0; i--) {
maxHeapify(nums, i, len);
}
}
private void maxHeapify(int[] nums, int i, int len) {
for (; i * 2 + 1 <= len;) {
int lson = i * 2 + 1;
int rson = i * 2 + 2;
int large = i;
if (lson <= len && nums[lson] > nums[large]) {
large = lson;
}
if (rson <= len && nums[rson] > nums[large]) {
large = rson;
}
if (large != i) {
swap(nums, i, large);
i = large;
} else {
break;
}
}
}
private void swap(int[] nums, int i, int j) {
int t = nums[i];
nums[i] = nums[j];
nums[j] = t;
}
}
复杂度分析
时间复杂度:O(nlogn)。初始化建堆的时间复杂度为 O(n),建完堆以后需要进行 n−1 次调整,一次调整(即 maxHeapify) 的时间复杂度为 O(logn),那么 n−1 次调整即需要 O(nlogn) 的时间复杂度。因此,总时间复杂度为 O(n+nlogn)=O(nlogn)。
空间复杂度:O(1)。只需要常数的空间存放若干变量。