面试必问的六大排序算法,精心总结


起初是时间复杂度为O(n 2)级的三种排序算法:

一、冒泡排序

一边比较一边向后两两交换,将最大值/最小值,冒泡到最后一位。

public void bubbleSort(int []arr){
   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);
           }
       }
   }
}

优化算法:如果外侧for循环的第i次遍历没有发生交换,则可以直接跳出循环(已经有序了)。

public void bubbleSort(int []arr){
	   boolean isSort = false;
	    for(int i = 0; i < arr.length - 1; i++){
	        //如果上次遍历后没有发生过交换,则代表数组已经有序
	        if(isSort == true){ 
	            break;
	        }
	        isSort = true;
	        for(int j = 0; j < arr.length - 1 - i; j++){
	            if(arr[j] > arr[j + 1]){
	                //将大的换到右边
	                swap(arr, j, j + 1);
	                //如果发生了交换,则不能代表已经有序
	                isSort = false; 
	            }
	        }
	    }
}

复杂度分析:

时间复杂度:比较的次数为(n-1)+(n-2)+(n-3)+…+1 = n2/2,所以时间复杂度是O(n2);
空间复杂度:只用到i、j两个变量,空间复杂度只有O(1);

稳定性分析

因为array[j] = array[j + 1]时,可以不交换,所以是稳定的。

练习例题

将数组排成最小的数
将数组拼接成最小数字,本质上就是个排序问题!只不过要变换排序规则:新的规则为:判断两个数分别前后拼接后谁大谁小。
移动零
用冒泡的方法,将0放到尾部。新的规则为:遇见0就交换。


二、选择排序

双重循环遍历数组,经过一轮比较,找到最大的元素下标,将其交换到队尾
优点:可以实现局部有序

public void selectSort(int []arr){
     for(int i = 0; i < arr.length; i++){
          int max_index = 0;
          for(int j = 0; j < arr.length - i; j++){
              //从0开始,遍历更新最大值坐标
              if(arr[j] > arr[max_index]){
                  max_index = j;
              }
          }
          swap(arr, max_index, arr.length - 1 - i);
      }
  }

理解实例:
在这里插入图片描述

复杂度

时间复杂度:O(n2);
空间复杂度:O(1)

稳定性

不稳定,上述实例中第一次交换将arr[0]和arr[5]交换后,打乱了原本arr[4]=0和arr[5]=0的相对顺序,因此不稳定。

练习例题

数组中第k个最大元素
利用选择排序的优点,实现数组前部分局部有序。每次排序将最大值交换到开头,循环遍历k此后,返回数组中第k个元素即可。

三、插入排序

理解:打扑克时,一边抓牌一边给扑克牌排序,每次模一张牌,就将它插到手上已有的牌中的合适位置,逐渐完成整个排序。
插入方法:在新数字插入过程中,不断与前面的数字交换,直到找到适合自己的位置。

public static void insertSort(int[] arr) {
    // 从第二个数开始,往前插入数字
    for (int i = 1; i < arr.length; i++) {
        // j 记录当前数字下标
        int j = i;
        // 当前数字比前一个数字小,则将当前数字与前一个数字交换
        while (j >= 1 && arr[j] < arr[j - 1]) {
            swap(arr, j, j - 1);
            // 更新当前数字下标
            j--;
        }
    }
}

整个过程就好像已经有一些人按身高(从低到高)排成了一排,这时一个新人想要插进去,他会从最后一个人开始对比身高,如果前面的人比他高,他就和前面的人换位置。直到换到前一个人比他低了为止。

复杂度分析

时间复杂度:还是两层循环,复杂度为:O(n2);
空间复杂度:只需要临时变量j,空间复杂度为O(1);

稳定性分析

稳定,每次插入后,向前交换时的判断条件中,如果相等了就停止了,不破坏相对顺序。

练习例题

对链表进行插入排列
原理上就是插入排序,只不过考验对单向链表操作的熟练度。


时间复杂度是O(nlogn)级排序算法:

四、堆排序

基本原理也是一种选择排序,只是不在使用遍历的方式查找无序区间的最大的数,而是通过堆来选择无序区间的最大数。

堆排序过程如下:

  1. 用数列构成一个大根堆,取出堆顶的数字;(根节点的值 ≥ 子节点的值,这样的堆被称之为最大堆,或大顶堆;)
  2. 调整剩余的数字,构建出新的大根堆,再次取出堆顶的数字;
  3. 循环往复,完成整个排序。

整体的思路如上,我们需要解决的问题有两个:

  1. 怎么用数列构建出一个大根堆;
  2. 取出堆顶的数字后,如何将剩余的数字调整成新的大根堆。

(1)构建大根堆

【思路】

  • 将新加入的数,不断的向上换,且只用跟父节点进行比较(新的数下标index利用【(i - 1) / 2】计算出父节点的index,并父节点进行PK
  • 如果一直比父大,就一直往上换,直到不比此时的父大了或已经到头了,那就停止。

将此过程称之为heapInsert过程。

因此可以将数组从头开始每个元素进行一遍heapInsert过程,来构建大根堆。

【案例:heapInsert(int[] arr, int index), arr=[4,6,8,5,9], index = 4】
在这里插入图片描述
【heapInsert过程】(logn级别调整代价)

public static void heapInsert(int []arr, int index){
    //当前的数如果大于父节点的数,就与父位置进行交换
    while(arr[index] > arr[(index - 1) / 2]){
        swap(arr, index, (index - 1) / 2);
        //交换完后,index来到了父的位置,继续进行while判断
        index = (index - 1) / 2;
    //停止条件如上述思路讲解;
    }
}

(2)调整堆

如果需要将大根堆中最大的数找出来,并且去掉,然后构建新的大根堆出来,此时应该怎么办?

  1. 给用户返回的最大值,一定是大根堆中index=0位置的数(根节点上);
  2. 拿去根节点后,将已经形成的堆结构最后的一个数放在index= 0位置上;
  3. 从头节点开始,先在该头节点左右孩子中选取一个最大节点,然后与它pk。如果小于该子节点了,就将头结点与此子节点值进行交换;
  4. 重复步骤3,直到此节点再也没有了子节点 或 此节点大于左右子节点中的最大值,就停止。

此过程称为:heapify

【实例】
在这里插入图片描述
【heapify过程】(logn级别调整代价)

//某个数在index位置,能够往下移动
public static void heapify(int []arr, int index, int heapSize){
    int left = index * 2 + 1;//左孩纸的下标
    while(left < heapSize){
        //两个孩子,谁的值大,就把下标给largest
        int largest = left + 1 < heapSize && arr[left] < arr[left + 1]? left + 1 : left;
        //父和较大的孩子之间,谁的值大,把谁的下标给largest
        largest = arr[largest] < arr[index] ? index : largest;
        if(largest == index){
            break;
        }
        swap(arr,largest,index);//交换值
        index = largest;//交换值后,下标换到大于他的子节点下标
        left = index * 2 + 1;//得到此时的左子节点
        //进行下一轮判断
    }
}

(3)堆排

public static void heapSort(int []arr){
    if(arr == null || arr.length < 2){
        return ;
    }
    //将数组大根堆化
    for(int i = 0; i < arr.length; i++){//O(N)        
        heapInsert(arr,i);//O(log N)
}
    //此时的长度
    int heapSize = arr.length;
    //交换头结点和末尾节点的值(堆区间的最大值),同时交换后尾结点的值在某种意义上已经不在算入我们的堆区间内,此时堆区间的长度-1;
    swap(arr,0,--heapSize);
    while(heapSize > 0){//O(N)
        heapify(arr,0,heapSize);//O(log N) 
        swap(arr,0,--heapSize);//O(1)
    }
}

复杂度分析

时间复杂度:O(nlogn)
空间复杂度:O(1)

稳定性分析:

不稳定。

练习例题

最小的k个数

五、快速排序

  • 从数组中抽出一个数(称之为基数(pivot))
  • Partition:遍历整个待排序数组区间,将比基准值小的(可以包含相等的)放到基准值的左边,将比基准值大的(可以包含相等的)放到基准值右边;
  • 采用分治思想,对左右两个小区间重复Partition,直到小区间的长度等于1或0

(1)快排1.0

过程实现

public void quickSort(int[] array){
        quickSortInternal(array, 0, array.length - 1);
    }

//[left,right]是待排序区间
public void quickSortInternal(int []array, int left, int right){
    if(left == right){
        return;
    }
    if(left > right){
        return;
    }
    //选取第一个数作为基准值pivot
    int pivotIndex = partition(array, left, right);

    //[left,pivotIndex - 1]都是小于基准值的
    //[pivotIndex + 1, right]都是大于基准值的
    quickSortInternal(array,left, pivotIndex - 1);
    quickSortInternal(array, pivotIndex + 1, right);
}

partition双指针分区算法原理

在这里插入图片描述

public int partition(int[] array, int left, int right){
        int l = left;
        int r = right;
        int pivot = array[left];
        //选择那一端开头值为pivot,就必须从另一端开始对比!
        while(l < r){
            while(l < r && array[r] >= pivot){
                r--;
            }
            while(l < r && array[l] <= pivot){
                l++;
            }
            swap(array,l, r);
        }

        swap(array, left, l);
        return l;
    }

(2)快排2.0

  1. 将数组通过pivot划分Wie三个位置:<pivot、 ==pivot、>pivot
    每次只用处理小于和大于区间即可,减少了工作量
  2. 用随机数进行pivot的选取。

在这里插入图片描述
需要三个下标:

  1. 小于区间的下标less = left - 1
  2. 大于区间的下标more = right
  3. 当前值的下标left

操作步骤:

  • arr[right]作为pivot基准值
  • 根据判断实时arr[left]和pivot值的大小来更新大小区间
    -1. 如果当前值小于基准值:swap(arr, ++less, left++) 维护了小于、等于区间
    -2. 如果当前值大于基准值:swap(arr, --more, left),left不往前是因为还需要判断交换过来的值,并且维护了大于区间
    -3. 如果当前值等于基准值:left++,当前值下标直接往前,更新等于区间
public void quickSort(int[] array){
        quickSortInternal(array, 0, array.length - 1);
    }

    public void quickSortInternal(int []arr, int left, int right){
        if(left < right){
            swap(arr, left + (int)(Math.random() * (right - left + 1)), right); //随机抽取数值
            int[] p = partition(arr, left, right);
            quickSortInternal(arr, left, p[0] - 1);
            quickSortInternal(arr, p[1] + 1, right);
        }

    }
    //这是一个处理array[left...right]的函数
    //默认以array[right]做划分,pivot=arr[right], 分为三个区:<pivot  ==pivot  >pivot
    //返回等于区域(左边界、右边界),所以返回一个长度为2的数组res,res[0],res[1]
    public int[] partition(int[] array, int left, int right){
        int less= left- 1; // <区间的右边界
        int more = right; // >区间的左边界
        int pivot = array[right];
        while(left < more){ // left代表当前数的位置;array[right]代表划分值 
            if(array[left] < pivot){  // 当前数 < 划分值
                swap(array, ++less, left++);
            }else if(array[left] > pivot){ // 当前数 > 划分值
                swap(array, --more, left);
            }else{
                left++;
            }
        }
        swap(array, more, right);
        return new int[]{less + 1, more};      
    }

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

性能分析

在这里插入图片描述

六、归并排序

利用归并思想实现:

  1. 先将数组递归分解;
  2. 再对分解后的两个数组进行比较合并(后序遍历)

在这里插入图片描述

public void mergeSort(int [] nums){
        mergeSortInternal(nums, 0, nums.length - 1);
    }

    public void mergeSortInternal(int[] nums, int start, int end){
        if(start >= end){
            return;
        }
        int mid = (start + end) / 2;
        mergeSortInternal(nums, start, mid);
        mergeSortInternal(nums, mid + 1, end);

        merge(nums, start, mid, end);
    }

    public void merge(int[] nums, int start, int mid, int end){
        int[] res = new int[end - start + 1];
        int i = start;
        int j = mid + 1;
        int index = 0;

        while(i <= mid && j <= end){
            if(nums[i] < nums[j]){
                res[index++] = nums[i++]; 
            }else{
                res[index++] = nums[j++];
            }
        }
        while(i <= mid){
            res[index++] = nums[i++];
        }
        while(j <= end){
            res[index++] = nums[j++];
        }

        for(int k = 0; k < res.length; k++){
            nums[start + k] = res[k];
        }
    }

稳定性分析:

稳定

排序总结

在这里插入图片描述

  • 7
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值