目录
给你一个整数数组 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
来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/sort-an-array
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
冒泡排序
基本思路:
1.从头开始让相邻的两个元素进行比较,符合条件就交换位置,这样就把最大值或者最小值放到数组的最后面了;
2.接着再从头开始两两比较交换,直到把最大值或者最小值放到数组的倒数第二位。(即不需要与最后一位数进行对比).....以此类推,直到排序完成。
时间复杂度O(N^2),空间复杂度O(1)
一般冒泡排序
public void bubbleSort(int[] nums) {
int len = nums.length;
//外层循环进行n-1趟冒泡排序
for (int i = 0; i < len - 1; i++) {
//内层循环从第一个元素开始每两个进行比较,得出最大/最小的元素
//这里减去 i 是因为每次外层循环都会确定一个最值(已经排好序了)
for (int j = 0; j < len - i - 1; j++) {
if (nums[j] > nums[j + 1]) {
swap(nums, j, j + 1);
}
}
}
}
private void swap(int[] nums, int i, int j) {
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
冒泡排序优化
在遍历的过程中,提前检测到数组是有序的,可以结束排序,这样如果输入数据是有序的,可以避免排序过程。
public void bubbleSortWithFlag(int[] nums) {
int len = nums.length;
for (int i = 0; i < len - 1; i++) {
//先默认数组有序的,只要发生一次交换,就必须进行下一轮比较
//如果在内层循环中,都没有执行一次交换操作,说明此时数组已经是升序数组
boolean sorted = true;
for (int j = 0; j < len - i - 1; j++) {
if (nums[j] > nums[j + 1]) {
swap(nums, j, j + 1);
sorted = false;
}
}
if (sorted) {
break;
}
}
}
选择排序
基本思路:
1.首先在未排序的数列中找到最小(or最大)元素,然后将其存放到数列的起始位置。
2.接着再从剩余未排序的元素中继续寻找最小(or最大)元素,然后放到已排序序列的末尾。
3.以此类推,直到所有元素均排序完毕。
时间复杂度O(N^2),空间复杂度O(1)
public void selectSort(int[] nums) {
int len = nums.length;
//把数组分为有序和无序两部分,[0,i)区间是有序的
for (int i = 0; i < len - 1; i++) {
int minIndex = i;
//找到区间[i+1,len-1]里最小的元素索引
for (int j = i + 1; j < len; j++) {
if (nums[j] < nums[minIndex]) {
minIndex = j;
}
}
//每一轮选择最小元素交换到未排定部分的开头
swap(nums, i, minIndex);
}
}
插入排序
基本思路:
1.将一个记录插入到已经排好序的有序表中,从而得到一个新的,记录数增加1的有序表。
2.实现时,使用双层循环,外层循环对除了第一个元素之外的所有元素,内层循环对当前元素前面有序表进行待插入位置查找,进行移动。
3.类似于生活中扑克牌游戏,会习惯性地把手上的牌按照从小到大排好,每次拿到新牌时就是插入排序的过程。
时间复杂度O(N^2),空间复杂度O(1)
直接插入排序
public void insertionSort(int[] nums) {
//外层循环从数组中第二个元素开始遍历
for (int i = 1; i < nums.length; i++) {
//先保存这个元素
int temp = nums[i];
int j = i;
//如果前一个元素大于temp,将该元素之前的逐个后移,留出空位
while (j > 0 && nums[j - 1] > temp) {
nums[j] = nums[j - 1];
j--;
}
//将temp放到合适的位置
nums[j] = temp;
}
}
二分插入排序
因为左边数组是有序的,所以当元素数量比较多时,可以采用二分定位新元素插入位置。
public void binaryInsertionSort(int[] nums) {
for (int i = 1; i < nums.length; i++) {
int temp = nums[i];
int left = 0, right = i - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] > temp) {
right = mid - 1;
} else {
left = mid + 1;
}
}
int j = i;
//左边索引右边的数据 到 要排序的数据之前的数据 都往后移动一位
while (j > left) {
nums[j] = nums[j - 1];
j--;
}
//循环结束的条件是j<=left,要插入排序的数据插入到左边索引的位置
nums[j] = temp;
}
}
归并排序
感谢Leetcode大神liweiwei1419的详细解法,传送门复习基础排序算法(Java)
感谢 labuladong的详细解法,传送门归并排序的正确理解方式及运用
基本思路:先把左半边数组排好序,再把右半边数组排好序,然后把两边数组合并。
时间复杂度O(NlogN),空间复杂度O(N)
public void mergeSort(int[] nums, int left, int right, int[] temp) {
//小区间使用插入排序
if (right - left <= INSERTION_SORT_THRESHOLD) {
insertionSort(nums, left, right);
return;
}
int mid = left + (right - left) / 2;
mergeSort(nums, left, mid, temp);
mergeSort(nums, mid + 1, right, temp);
//如果数组的这个子区间本身有序,无需合并
if (nums[mid] <= nums[mid + 1]) {
return;
}
merge(nums, left, mid, right, temp);
}
private void merge(int[] nums, int left, int mid, int right, int[] temp) {
//先把nums[left,right]copy到temp数组中,以便合并后的结果能够直接存入nums
for (int i = left; i <= right; i++) {
temp[i] = nums[i];
}
int i = left, j = mid + 1;
for (int k = left; k <= right; k++) {
if (i == mid + 1) {
//左边数组已经排好序
nums[k] = temp[j++];
} else if (j == right + 1) {
//右边数组已经排好序
nums[k] = temp[i++];
} else if (temp[i] <= temp[j]) {
//注意写成 < 就丢失了稳定性(相同元素原来靠前的排序以后依然靠前)
nums[k] = temp[i++];
} else {
nums[k] = temp[j++];
}
}
}
快速排序
快速排序的思想:
1.从数组中任意选取一元素作为pivot中心轴。
2.将大于pivot的数字放在pivot的右边,将小于pivot的数字放在pivot的左边。
3.分别对左右子序列重复做前两步操作,直到各区间只有一个数。
时间复杂度O(NlogN),空间复杂度logN
刨坑法
感谢xiehongfeng的详细解释,传送门 “《算法导论》之‘排序’”:快速排序_51CTO博客_各种排序算法
public void quickSort(int[] nums, int left, int right) {
if (left >= right) {
return;
}
int p = partition(nums, left, right);
quickSort(nums, left, p - 1);
quickSort(nums, p + 1, right);
}
//pivot在排序之前就拿出来,空一个位置
//然后左右指针交替扫描,遇到不符合情况的就把那个不符合的数字放到空位置,直至左右指针重合
//在排序过程中pivot是一直在序列之外的,在一趟排序之后再把pivot放回唯一的空位置就行
private int partition(int[] nums, int left, int right) {
//即nums[left]就是第一个坑,因为pivot选取的是左边第一个数,所以先从右往左开始移动
int pivot = nums[left];
int i = left, j = right;
while (i < j) {
//从右往左移动,跳过大于pivot的元素
while (i < j && nums[j] >= pivot) {
j--;
}
//遇到小于pivot的元素,将其放到左边空出的坑位中,并移动i
if (i < j) {
nums[i] = nums[j];
i++;
}
//从左往右移动,跳过小于pivot的元素
while (i < j && nums[i] <= pivot) {
i++;
}
//遇到大于pivot的元素,将其放到右边空出的坑位中,并移动j
if (i < j) {
nums[j] = nums[i];
j--;
}
}
//退出循环时,i=j,此时将pivot指向的元素放到该坑位中
nums[j] = pivot;
return j;
}
交换指针元素法
1.先从数组中选取一个数当做基准数,一般选择最左边的数当做基准数,然后从两边进行检索。
2.先从右边检索比基准数小的,再从左边检索比基准数大的。如果检索到了,就停下,然后交换这两个元素。然后再继续检索。
3.直到左右指针相遇,停止检索,把基准数和相遇位置的元素交换,此时第一轮交换结束。基准数左边都比它小,右边都比它大。
4.以后先排基准数左边,排完以后再排基准数右边,方式和第一轮一样。
public void quickSort(int[] nums, int left, int right) {
if (left >= right) {
return;
}
int p = partition(nums, left, right);
quickSort(nums, left, p - 1);
quickSort(nums, p + 1, right);
}
private int partition(int[] nums, int left, int right) {
int pivot = nums[left];
int i = left, j = right;
while (i < j) {
//因为基准数是左边第一个数,所以先从右边开始,跳过大于pivot的元素
while (i < j && nums[j] >= pivot) {
j--;
}
//从左边开始,跳过小于pivot的元素
while (i < j && nums[i] <= pivot) {
i++;
}
//此时i指向的元素大于pivot,j指向的元素小于pivot,交换这两个指针指向的元素
if (i < j) {
swap(nums, i , j);
}
}
//循环结束,此时i==j,将相遇位置的元素和pivot交换
//此时第一轮交换结束。基准数左边都比它小,右边都比它大
nums[left] = nums[i];
nums[i] = pivot;
return i;
}
private void swap(int[] nums, int i, int j) {
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
快速排序优化
上面两种方法都是选取最左边元素作为pivot,如果出现顺序数组或者逆序数组,快速排序会变得非常慢,因此一定要随机化选择切分元素(pivot),同时对于小区间内的数组,使用插入排序效率更高。
private static final int INSERTION_SORT_THRESHOLD = 47;
public final Random RANDOM = new Random();
private void quickSort(int[] nums, int left, int right) {
//小区间内排序,使用插入排序
if (right - left <= INSERTION_SORT_THRESHOLD) {
insertionSort(nums, left, right);
return;
}
int p = partition(nums, left, right);
quickSort(nums, left, p - 1);
quickSort(nums, p + 1, right);
}
private int partition(int[] nums, int left, int right) {
//随机生成[left, right + 1)区间内的数据
int randomIndex = left + RANDOM.nextInt(right - left + 1);
swap(nums, randomIndex, left);
//避免出现耗时的极端情况,随机取值pivot
int pivot = nums[left];
//定义[left, i) <= pivot, (j,right] > pivot,遍历时要正确维护区间
int i = left + 1, j = right;
while (true) {
//跳过小于pivot的元素
while (i <= right && nums[i] < pivot) {
i++;
}
//跳过大于pivot的元素
while (j > left && nums[j] > pivot) {
j--;
}
//此时满足条件[left, i) <= pivot && (j, right] > pivot,直接退出循环
if (i >= j) {
break;
}
//否则,出现[left, i) > pivot || (j, right] <= pivot,进行元素值交换
swap(nums, i, j);
i++;
j--;
}
//跳出循环,pivot需要放置在这样一个位置,即该位置左侧的所有元素都不大于中枢元素,右侧的所有元素都不小于中枢元素
//此时索引i 会停在第一个不小于中枢元素的位置,索引 j 指向的是最后一个不大于中枢元素的位置
//j 位置的元素是中枢值应该放置的位置,因为它是从右向左遇到的最后一个不大于中枢值的元素。
//i 位置的元素可能大于中枢值,所以不能与中枢值交换。
swap(nums, left, j);
return j;
}
private void insertionSort(int[] nums, int left, int right) {
for (int i = left + 1; i <= right; i++) {
int temp = nums[i];
int j = i;
while (j > left && nums[j - 1] > temp) {
nums[j] = nums[j - 1];
j--;
}
nums[j] = temp;
}
}