vlambda博客
学习文章列表

一个快速排序,一个堆排序,一个归并排序,全部被张三防出去了啊

在经典的八大排序算法中,由于快速排序、堆排序和归并排序的时间复杂度可以达到O(nlogn),且复杂性较高,所以是面试中比较常问的点,今天我们一起来复习一下这三种排序算法。

说明:本文中所有的排序都是按升序进行排序

归并排序

基本思路:借助额外空间,将两个较短数组合并为一个更长的有序数组,整个过程递归执行,直到得到数组的长度等于原数组长度

算法思想:分治思想

算法过程:

  • 首先对数组进行等长划分,整个过程递归执行,直到子数组长度全部为1

  • 对两个子数组进行归并,由于子数组自身有序,因此两个子数组指针从头开始往后移动,将两个子数组指针指向值的更小值放入排序数组,同时该指针向后移动,直到两个指针都达到数组尾部,将得到的排序数组作为返回值返回,作为上一层递归的子数组。

  • 递归执行第二步,直到整个数组有序


归并排序的思想还是比较直观的,归并排序本身还分为自顶向下和自底向上,我们刚刚说的这种是自顶向下的思想。而自底向上的思路也是比较直观的,会在后面给出的代码中给出详细的注释,这里就不再赘述啦,思想上都是差不多的

public class Solution {
   // 自顶向下的归并排序
   public int[] sortArray(int[] nums) {
       int len = nums.length;

       //temp是辅助数组,用于存放两个子数组中的值
       int[] temp = new int[len];
       mergeSort(nums, 0, len - 1, temp);
       return nums;
  }


   private void mergeSort(int[] nums, int left, int right, int[] temp) {
       //此时子数组长度为一,划分子数组过程结束,
       //返回进入后续归并过程
       if(right==left)
           return ;

       //将原数组划分为两个等长子数组
       int mid = left + (right - left) / 2;

       //对左侧进行归并排序
       mergeSort(nums, left, mid, temp);
       //对右侧进行归并排序
       mergeSort(nums, mid + 1, right, temp);

       // 左侧区间最后一个元素下标为mid
       // 右侧区间第一个元素下标为mid+1
       // 左右数组都有序
       // 因此nums[mid] <= nums[mid + 1]
       // 说明left到right区间内完全有序
       // 直接返回无需再次排序
       if (nums[mid] <= nums[mid + 1]) {
           return;
      }

       //对两个子数组进行排序
       mergeOfTwoSortedArray(nums, left, mid, right, temp);
  }


   private void mergeOfTwoSortedArray(int[] nums, int left, int mid, int right, int[] temp) {

       //将元素拷贝到temp中,
       System.arraycopy(nums, left, temp, left, right + 1 - left);

       // i为左侧区间指针
       int i = left;
       // j为右侧区间指针
       int j = mid + 1;

       for (int k = left; k <= right; k++) {

           
           if (i == mid + 1) {
               //左侧区间到头了
               // 直接取右侧区间元素
               nums[k] = temp[j];
               j++;
          } else if (j == right + 1) {
               // 右侧区间到头了
               // 直接去左侧区间元素
               nums[k] = temp[i];
               i++;
          } else if (temp[i] <= temp[j]) {
               // 注意写成 < 就丢失了稳定性(相同元素原来靠前的排序以后依然靠前)
               nums[k] = temp[i];
               i++;
          } else {
               // 这部分的条件等价于temp[i] > temp[j]
               nums[k] = temp[j];
               j++;
          }
      }
  }
}

时间复杂度O(nlogn)

空间复杂度O(n)

稳定的排序算法


自底向上的归并排序代码

class Solution {
   //自底向上的归并排序
   public int[] sortArray(int[] nums) {
       //temp为辅助数组
       int[] temp = new int[nums.length];
       mergeSort(nums,0,nums.length-1,temp);
       return nums;
  }

   //进行自底向上的子数组划分
   public void mergeSort(int[] nums,int left ,int right , int[] temp){
       int length = nums.length;

       // i为当前一步的子数组长度
       // 初始为1,每次翻倍
       for( int i = 1 ; i < length ;i = i*2 ){
           // j为划分子数组的左指针,
           // 每次对两个数组进行归并
           // 每次自增2*i
           for( int j = 0 ; j< length - i ; j = j + i + i ){

               //对子数组进行归并
               //j为当前子数组左侧下标
               //j+i-1是中间指针
               //为了确保右指针不越界
               //Math.min(length-1, j+i+i-1)表示右指针
               merge(nums,j,j+i-1 , Math.min(length-1, j+i+i-1) , temp);
          }
      }
  }

   /**
    *
    * @param nums 排序后的数组
    * @param left 最左侧下标
    * @param mid 中间指针
    * @param right最右侧下标
    * @param temp 辅助数组
    */
   public void merge(int[] nums ,int left ,int mid ,int right , int[] temp){
       for( int i = left ; i <=right ; i ++){
           temp[i] = nums[i];
      }
       int i = left;
       int 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++];
      }

  }
   
}

时间复杂度O(nlogn)

空间复杂度O(n)


快速排序

基本思路:找到数组中的一个值,然后确定数组中比该值更小的值有多少个,然后把比该值更小的值放在该值左侧,比该值更大的值放在右侧,然后对左右两侧重复进行上述过程

算法思想:分治思想,对已确定位置的元素左右侧进行分治

class Solution {
   //快速排序算法
   public int[] sortArray(int[] nums) {
       quickSort(nums,0,nums.length-1);
       return nums;
  }

   public void quickSort(int[] nums ,int left ,int right ){
       
       //确保当前子数组长度至少为2
       if( left < right){
           //进行一步随机划分
           int i = randomizedPartition(nums,left,right);
           //对左侧进行快排
           quickSort(nums ,  left , i-1);
           //对右侧进行快排
           quickSort( nums , i +1 ,right);
      }
  }

   public int randomizedPartition(int[] nums, int left ,int right){
       
       //通过随机函数找到一个随机下标对子数组进行划分
       int i = new Random().nextInt(right - left + 1 ) + left;
       //将这个随机下标放到最右侧
       swap(nums ,right,i );
       return partition(nums,left,right);
  }
   public int partition(int[] nums ,int left ,int right){
       //将最右侧的值设为pivot
       int pivot = nums[right];
       int i = left-1;
       for( int j = left ; j <right ; j ++){
           //比pivot小的值则靠左进行放置
           if(nums[j] <= pivot){
               i++;
               swap(nums,i,j);
          }
      }
       
       //把pivot放到正确的下标i+1上
       swap(nums,i+1,right);
       //返回pivot正确下标
       return i+1;
  }

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

平均时间复杂度O(nlogn) ,最差时间复杂度O(n*n),最差时间复杂的情况会出现在数组原本就是降序排序,我们需要得到一个升序数组的时候,我们每次都要把最右侧的值放回靠左的位置,一共会进行n趟,每次遍历n个元素。为了减少这种情况的发生,我们加入了一个随机函数,可以尽量避免这种最差情况的发生。

空间复杂度O(nlogn)



堆排序

这里简单介绍一下堆排序的思想,首先将数组模拟成一个二叉堆,我们第一次遍历的时候从堆底开始,将更大的元素逐层向上交换,最后将最大元素放在堆顶,然后我们把对顶元素与最下一层最右侧的元素进行交换,并将最大元素从堆中删除。然后由于第一次遍历的时候,相对较大的元素都在相对靠上的位置,我们将堆顶元素向下沉,结束之后,把堆顶元素与最后一个元素进行交换,并把最大元素删除,重复以上过程直到堆为空即排序结束

public class Solution {

public int[] sortArray(int[] nums) {
   int len = nums.length;
   // 将数组整理成堆
   heapify(nums);

   // 循环不变量:区间 [0, i] 堆有序
   for (int i = len - 1; i >= 1; ) {
       // 把堆顶元素(当前最大)交换到数组末尾
       swap(nums, 0, i);
       // 逐步减少堆有序的部分
       i--;
       // 下标 0 位置下沉操作,使得区间 [0, i] 堆有序
       siftDown(nums, 0, i);
  }
   return nums;
}


//将数组整理成堆(堆有序)
private void heapify(int[] nums) {
   int len = nums.length;
   // 只需要从 i = (len - 1) / 2 这个位置开始逐层下移
   for (int i = (len - 1) / 2; i >= 0; i--) {
       siftDown(nums, i, len - 1);
  }
}

/**
* @param nums
* @param k   当前下沉元素的下标
* @param end [0, end] 是 nums 的有效部分
*/
private void siftDown(int[] nums, int k, int end) {
   while (2 * k + 1 <= end) {
       int j = 2 * k + 1;
       if (j + 1 <= end && nums[j + 1] > nums[j]) {
           j++;
      }
       if (nums[j] > nums[k]) {
           swap(nums, j, k);
      } else {
           break;
      }
       k = j;
  }
}

private void swap(int[] nums, int index1, int index2) {
   int temp = nums[index1];
   nums[index1] = nums[index2];
   nums[index2] = temp;
}

时间复杂度O(nlogn)

空间复杂度O(1)