给你一个整数数组 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]
分析:题目那是相当简单,但是本题你也可以自己给自己增加难度,比如,让你用8大常用排序算法来解决该问题呢?下面我们就来一个一个做出解释和分析。
一、冒泡算法
该算法应该是我们学习第一门编程语言时最先接触到的算法了。也是最简单的算法,其核心思想就是每次遍历整个数组,都把最大值向后置换到最后一位。循环数次后便可以得到有序数组:
public int[] sortArray(int[] nums) {
for(int i = 0; i < nums.length; i++){
for(int j = 0; j < nums.length - i - 1; j++){
if(nums[j] > nums[j+1]){
int a = nums[j];
nums[j] = nums[j+1];
nums[j+1] = a;
}
}
}
return nums;
}
该算法简单,但负责度高,在面试中一般复杂度这一关就过不去,在力扣上也是超时。
二、选择排序
选择排序应该是和冒泡排序一起接触学习的。因为选择排序和冒泡排序是两个对立的情况,冒泡排序是每一次把最大的放最后,而选择排序是每一次选择最小的放最前。
public int[] sortArray(int[] nums){
for(int i = 0; i < nums.length; i++){
for(int j = i+1; j < nums.length; j++){
if(nums[i] > nums[j]){
int a = nums[j];
nums[j] = nums[i];
nums[i] = a;
}
}
}
return nums;
}
该函数复杂度几乎等价于冒泡,所以理解即可,面试中应用不大。力扣上执行结果还是超时。
三、插入排序
插入排序也是一种非常经典的排序方法,它的核心思想是:
指针从第二个元素出发,然后向前找该元素的合适位置放入即完成一轮操作。也就是说当在执行第i轮时,其第i个元素之前的元素一定是有序的。
public int[] sortArray(int[] nums){
for(int i = 0; i < nums.length; i++){
int a = nums[i];
int j = i-1;
while(j >= 0 && a < nums[j]){
nums[j+1] = nums[j];
j = j - 1;
}
nums[j+1] = a;
}
return nums;
}
该算法复杂度就要小一些了,其在力扣上执行用时1812ms,已经是有效解决方法了。这主要是因为前面的元素都是有序的,所以在执行的时候减少了时间损耗。
4、折半插入排序
你如果明白了插入排序是在前面的有序序列中寻找一个合适的位置,那么你肯定能想到前面的有序序列为什么不用二分查找,而要使用逐层遍历呢?
public int[] sortArray(int[] nums){
int i,j,low,high,mid;
for( i=0;i<nums.length;i++ ){
int tmp = nums[i];
low = 0;high = i-1;
while(low<=high) {
mid = low+(high-low)/2;
if(nums[mid] > tmp){
high = mid - 1;
}else{
low = mid + 1;
}
}
for(j=i-1;j>=high+1;j--){
nums[j+1] = nums[j];
}
nums[high+1] = tmp;
}
return nums;
}
该方法效果有了巨大的提升,力扣执行用时11ms,几乎提升了上百倍的执行速度。在折半方法的实现中,记住我们要找的tmp能插入的左边界,也就是最小的大于tmp的值。
5、希尔排序
希尔排序是思想其实还是来自插入排序,只不过它不在是进行简单粗暴的直接插入排序了,而是将其按照一定大小分组(一般默认是总长度的一半作为第一次分组大小,随后每次缩小一半,直到为1),对每一个分组内的第一个元素执行插入排序,第二个元素执行插入排序,,,分组内最后一个元素执行插入排序。
public int[] sortArray(int[] nums){
for(int dk = nums.length/2;dk>=1;dk=dk/2){
// for(int i=dk; i<nums.length; i=i+dk) {
for(int i=dk; i<nums.length; i=i+1) {
if(nums[i]<nums[i-dk]){
int tmp = nums[i],j;
for(j = i-dk;j>=0&&tmp<nums[j];j-=dk){
nums[j+dk] = nums[j];
}
nums[j+dk]=tmp;
}
}
}
return nums;
}
在力扣上运行时间也是12ms,速度可以说很快了。其实前面的你都可以做个简单了解即可,真正有难度和面试常问的主要集中在下面三种排序上。
6、快速排序
快速排序是排序算法中非常经典和常用的方法,其在源码中也有应用。其核心思想就是:
从第一个元素开始,从右边往左找一个比它小的,放到该元素位置,在从该元素下一个位置开始往右找一个比它大的,放到刚刚移过来的右侧元素的位置。就这样重复一直到左右指针相遇结束,此时把该第一个元素放入到相遇节点处。我们的数组以该元素为分界点,左侧都小于它,右侧都大于它,那我们就可以分而治之了。
public int[] sortArray(int[] nums){
Test(nums, 0, nums.length-1);
return nums;
}
void Test(int[] nums, int left, int right){
if(left > right){
return;
}
int begin = left;
int end = right;
// int index = left;
int total = nums[begin];
while(begin < end){
while(begin < end && nums[end] > total){
end -= 1;
}
if(begin < end){
nums[begin] = nums[end];
begin++;
}
while(begin < end && nums[begin] < total){
begin++;
}
if(begin < end){
nums[end] = nums[begin];
end--;
}
}
nums[begin] = total;
Test(nums, left, begin-1);
Test(nums, begin+1, right);
}
可以发现,快速排序,其运行时间花费了883ms,但也算是很快的。比较插入排序划分1800多ms,,
7、归并排序
其实归并排序也含有分而治之的思想,所谓归并排序就是先让左右两侧都有序,在将两个有序数组合并即可。所以其核心就是:
找数组的中点,然后分成两段递归调用,一直分到不能再分位置,然后开始左右两侧有序数组合并,一层层回溯即可。
int[] tmp;
public int[] sortArray(int[] nums) {
tmp = new int[nums.length];
mergeSort(nums, 0, nums.length - 1);
return nums;
}
public void mergeSort(int[] nums, int l, int r) {
if (l >= r) {
return;
}
int mid = (l + r) >> 1;
mergeSort(nums, l, mid);
mergeSort(nums, mid + 1, r);
int i = l, j = mid + 1;
int cnt = 0;
while (i <= mid && j <= r) {
if (nums[i] <= nums[j]) {
tmp[cnt++] = nums[i++];
} else {
tmp[cnt++] = nums[j++];
}
}
while (i <= mid) {
tmp[cnt++] = nums[i++];
}
while (j <= r) {
tmp[cnt++] = nums[j++];
}
for (int k = 0; k < r - l + 1; ++k) {
nums[k + l] = tmp[k];
}
}
在最后位置对原数组进行修改替换即可。其力扣运行时间花费11ms,这也是最块的一种算法了。
8、堆排序
堆排序无疑是最难的一种排序算法,你需要的做的有:
1)先对整个数组建立堆,大根堆或者小根堆
2)将堆顶元素和最后一个元素交互位置,然后重新根据除最后一个元素外前面的元素再次整理建堆,依次循环便可以得到结果。
难点在于建堆以及如何整理堆。当我们传入一组数组进行建堆时,我们需要从下向上建堆,叶子节点不需要建堆,因为就一个节点,自己就是堆。所以我们就从最底下第一个有孩子的节点上开始建堆,该节点就是len / 2处的节点。逐层向上,一直到最顶上,堆创建完成。
在每一次循环建堆时,我们都要做一个判断,看看当前根节点和左右孩子节点是不是符合堆规定,如果符合,我们就可以直接终止本次循环,进入到前一个节点的建堆过程中,这是因为我们采用自下而上建堆,所以下面的堆自然都是有序的,只要当前节点和左右孩子符合堆设定,那么以当前节点为跟的子树肯定符合堆,因此就不需要调整,可以进入前一个节点的调整了。但是,如果当前节点和左右孩子不符合堆的设定呢?那我们就需要将(比如堆设定为大根堆)最大值调整到父亲节点,然后对调整的子节点进行循环分析是否符合大根堆设定,因此,我们需要一个循环,从当前节点一直到最下面最后的一个有孩子节点。来进行逐次调整堆。在循环内,如果父节点比左右孩子节点中都大,那么就说明不需要调整了,break即可。如果不是,那就将子节点中最大的调整到父节点,然后将该子节点送入到遍历中,向下调整即可。
public int[] sortArray(int[] nums) {
heapSort(nums);
return nums;
}
public void heapSort(int[] nums) {
int len = nums.length - 1;
buildMaxHeap(nums, len);
for (int i = len; i >= 1; --i) {
swap(nums, i, 0);
len -= 1;
maxHeapify(nums, 0, len);
}
}
public void buildMaxHeap(int[] nums, int len) {
// 从倒数第二层开始的往下建立大根堆,最后一层不能建成堆,因此就直接从倒数第二层最后一个有孩子的节点开始建堆
for (int i = len / 2; i >= 0; --i) {
maxHeapify(nums, i, len);
}
}
public void maxHeapify(int[] nums, int i, int len) {
for (; (i << 1) + 1 <= len;) {
int lson = (i << 1) + 1;
int rson = (i << 1) + 2;
int large;
if (lson <= len && nums[lson] > nums[i]) {
large = lson;
} else {
large = i;
}
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 temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}