常见排序算法

0. 概述

排序算法可以分为内部排序和外部排序:

  • 内部排序是数据记录在内存中进行排序。
  • 外部排序是因排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存。

在这里插入图片描述

1. 直接插入排序(稳定)

通常人们整理桥牌的方法是一张一张的来,将每一张牌插入到其他已经有序的牌中的适当位置。在计算机的实现中,为了要给插入的元素腾出空间,我们需要将其余所有元素在插入之前都向右移动一位。

一般来说,插入排序都采用in-place在数组上实现。具体算法描述如下:

  • 从第一个元素开始,该元素可以认为已经被排序
  • 默认从第二个数据开始比较。
  • 如果第二个数据比第一个小,则交换。然后在用第三个数据比较,如果比前面小,则插入(狡猾)。否则,退出循环
  • 说明:默认将第一数据看成有序列表,后面无序的列表循环每一个数据,如果比前面的数据小则插入(交换)。否则退出。
public static void main(String[] args) {

        int arr[] = {7, 5, 3, 2, 4};

        //插入排序
        for (int i = 1; i < arr.length; i++) {
            //外层循环,从第二个开始比较
            for (int j = i; j > 0; j--) {
                //内存循环,与前面排好序的数据比较,如果后面的数据小于前面的则交换
                if (arr[j] < arr[j - 1]) {
                    int temp = arr[j - 1];
                    arr[j - 1] = arr[j];
                    arr[j] = temp;
                } else {
                    //如果不小于,说明插入完毕,退出内层循环
                    break;
                }
            }
        }
    }

时间复杂度:
在这里插入图片描述
插入排序所需的时间取决于输入元素的初始顺序。例如,对一个很大且其中的元素已经有序(或接近有序)的数组进行排序将会比随机顺序的数组或是逆序数组进行排序要快得多。

2. 冒泡排序(稳定)

  • a、冒泡排序,是通过每一次遍历获取最大/最小值
  • b、将最大值/最小值放在尾部/头部
  • c、然后除开最大值/最小值,剩下的数据在进行遍历获取最大/最小值
public static void main(String[] args) {

        int arr[] = {8, 5, 3, 2, 4};

        //冒泡
        for (int i = 0; i < arr.length; i++) {
            //外层循环,遍历次数
            for (int j = 0; j < arr.length - i - 1; j++) {
                //内层循环,升序(如果前一个值比后一个值大,则交换)
                //内层循环一次,获取一个最大值
                if (arr[j] > arr[j + 1]) {
                    int temp = arr[j + 1];
                    arr[j + 1] = arr[j];
                    arr[j] = temp;
                }
            }
        }
    }

排序过程:
在这里插入图片描述

时间复杂度:
在这里插入图片描述

  • 冒泡排序是最容易实现的排序, 最坏的情况是每次都需要交换, 共需遍历并交换将近n²/2次, 时间复杂度为O(n²).
  • 最佳的情况是内循环遍历一次后发现排序是对的, 因此退出循环, 时间复杂度为O(n). 平均来讲, 时间复杂度为O(n²). 由于冒泡排序中只有缓存的temp变量需要内存空间, 因此空间复杂度为常量O(1).

3. 选择排序(不稳定)

  • a、将第一个值看成最小值
  • b、然后和后续的比较找出最小值和下标
  • c、交换本次遍历的起始值和最小值
  • d、说明:每次遍历的时候,将前面找出的最小值,看成一个有序的列表,后面的看成无序的列表,然后每次遍历无序列表找出最小值。
public void sort3(int arr[]) {
        for(int i = 0; i < arr.length; i++){
            int min = arr[0];
            int index = i;
            for(int j = i + 1; j <arr.length; j++){
                if(min > arr[j]){
                    index = j;
                    min = arr[j];
                }
            }
            int temp = arr[i];
            arr[i] = min;
            arr[index] = temp;
        }
        System.out.println("选择排序,排序后--------------------");
    }

排序过程:
在这里插入图片描述

4. 希尔排序(不稳定)

希尔排序也是一种插入排序,它是简单插入排序经过改进之后的一个更高效的版本,也称为缩小增量排序,同时该算法是冲破O(n2)的第一批算法之一。

  • 希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。
  • 简单插入排序很循规蹈矩,不管数组分布是怎么样的,依然一步一步的对元素进行比较,移动,插入,比如[5,4,3,2,1,0]这种倒序序列,数组末端的0要回到首位置很是费劲,比较和移动元素均需n-1次。
  • 而希尔排序在数组中采用跳跃式分组的策略,通过某个增量将数组元素划分为若干组,然后分组进行插入排序,随后逐步缩小增量,继续按组进行插入排序操作,直至增量为1。希尔排序通过这种策略使得整个数组在初始阶段达到从宏观上看基本有序,小的基本在前,大的基本在后。然后缩小增量,到增量为1时,其实多数情况下只需微调即可,不会涉及过多的数据移动。

看下希尔排序的基本步骤:

  • 我们选择增量gap=length/2,缩小增量继续以gap = gap/2的方式,这种增量选择我们可以用一个序列来表示,{n/2,(n/2)/2…1},称为增量序列。
  • 希尔排序的增量序列的选择与证明是个数学难题,我们选择的这个增量序列是比较常用的,也是希尔建议的增量,称为希尔增量,但其实这个增量序列不是最优的。此处我们做示例使用希尔增量。
    在这里插入图片描述
	public static void swap(int []arr,int a,int b){
        int temp = arr[a];
        arr[a] = arr[b];
        arr[b] = temp;
    }
    
	 /**
     * 希尔排序 针对有序序列在插入时采用交换法
     * @param arr
     */
    public static void sort(int []arr){
        //增量gap,并逐步缩小增量
       for(int gap=arr.length/2;gap>0;gap/=2){
           //从第gap个元素,逐个对其所在组进行直接插入排序操作
           for(int i=gap;i<arr.length;i++){
               int j = i;
               while(j-gap>=0 && arr[j]<arr[j-gap]){
                   //插入排序采用交换法
                   swap(arr,j,j-gap);
                   j-=gap;
               }
           }
       }
    }

排序过程:
在这里插入图片描述

5. 快排(不稳定)

  • 基本思路: 快速排序每一次都排定一个元素(这个元素呆在了它最终应该呆的位置),然后递归地去排它左边的部分和右边的部分,依次进行下去,直到数组有序;
  • 算法思想: 分而治之(分治思想),与「归并排序」不同,「快速排序」在「分」这件事情上不想「归并排序」无脑地一分为二,而是采用了 partition 的方法(书上,和网上都有介绍,就不展开了),因此就没有「合」的过程。
  • 实现细节(注意事项):(针对特殊测试用例:顺序数组或者逆序数组)一定要随机化选择切分元素(pivot),否则在输入数组是有序数组或者是逆序数组的时候,快速排序会变得非常慢(等同于冒泡排序或者「选择排序」);

下面是一个常规的快排:

	private void quickSort(int[] arr, int left, int right) {
        // 子数组长度为 1 时终止递归
        if (left >= right) 
        	return;
        // 哨兵划分操作(以 arr[l] 作为基准数)
        int i = left, j = right;
        while (i < j) {
            while (i < j && arr[j] >= arr[left]) j--;		
            while (i < j && arr[i] <= arr[left]) i++;
            swap(arr, i, j);
        }
        swap(arr, i, left);
        // 递归左(右)子数组执行哨兵划分
        quickSort(arr, left, i - 1);
        quickSort(arr, i + 1, right);
    }
    private void swap(int[] arr, int i, int j) {
        int tmp = arr[i];
        arr[i] = arr[j];
        arr[j] = tmp;
    }

排序过程:
在这里插入图片描述
随机选取哨兵节点:

**class Solution {
    public int[] getLeastNumbers(int[] arr, int k) {
        quickSort(arr, 0, arr.length - 1);
        return Arrays.copyOf(arr, k);
    }
    private void quickSort(int[] arr, int left, int right) {
        // 子数组长度为 1 时终止递归
        if (left >= right) return;
        
        // 哨兵划分操作,选择pivot用作基准,将最左边值与pivot交换位置
        int i = left, j = right;
        int pivot = left + (int) (Math.random() * (right - left + 1));
        swap(arr, pivot, left);

        while (i < j) {
            while (i < j && arr[j] >= arr[left]) j--;		//从左往右找到比pivot大的
            while (i < j && arr[i] <= arr[left]) i++;		//从右往左找到比pivot小的
            swap(arr, i, j);
        }
        swap(arr, i, left);
        // 递归左(右)子数组执行哨兵划分
        quickSort(arr, left, i - 1);
        quickSort(arr, i + 1, right);
    }
    private void swap(int[] arr, int i, int j) {
        int tmp = arr[i];
        arr[i] = arr[j];
        arr[j] = tmp;
    }
}**

快慢双指针 + 分治:

  • 先随机选择一个中间值pivot作为比较的基准,因此比这个基准小的放到左边,比这个基准大的放到右边
  • 把选择的基准放到最左边,也就是nums[low]和nums[pivot]交换位置
  • 慢指针 i 从low位置开始,指向比基准小的数字;快指针 j 从low + 1位置开始遍历
  • j <= high进入循环
    • 如果nums[j]比基准小,nums[i + 1]和nums[j]交换位置,并且i + 1
    • j每次循环 + 1
  • 循环结束后,当前 i 指针所在位置即为数组中比base小的最后一个位置,将其和最左边的base交换位置,也就是交换nums[low]和nums[i];交换完后,i位置之前的都是比它小的,i位置之后的都是比它大的
  • 返回i,该位置的元素已排序完成,就是已经在它该在的位置了,接下来要排序i之前的和i之后的元素了
class Solution {
    public int[] sortArray(int[] nums) {
        quickSort(nums, 0, nums.length - 1);
        return nums;
    }

    private void quickSort(int[] nums, int low, int high){
        if(low < high){
            int mid = partition(nums, low, high);
            quickSort(nums, low, mid - 1);
            quickSort(nums, mid + 1, high);
        }
    }

    private int partition(int[] nums, int low, int high){
        int pivot = low + (int) (Math.random() * (high - low + 1));
        swap(nums, pivot, low);
        int i = low, j = low + 1;
        while (j <= high){
            if(nums[j] < nums[low]){
                swap(nums, j, ++i);
            }
            j++;
        }
        swap(nums, low, i);
        return i;
    }

    private void swap(int[] nums, int i, int j){
        int tmp = nums[i];
        nums[i] = nums[j];
        nums[j] = tmp;
    }
}

6. 归并排序(稳定)

在这里插入图片描述
可以看到这种结构很像一棵完全二叉树,本文的归并排序我们采用递归去实现(也可采用迭代的方式去实现)。分阶段可以理解为就是递归拆分子序列的过程,递归深度为log2n。

合并相邻有序子序列:

  • 再来看看治阶段,我们需要将两个已经有序的子序列合并成一个有序序列,比如上图中的最后一次合并,要将[4,5,7,8]和[1,2,3,6]两个已经有序的子序列,合并为最终序列[1,2,3,4,5,6,7,8],来看下实现步骤。
    在这里插入图片描述
    在这里插入图片描述
package sortdemo;

import java.util.Arrays;

/**
 * Created by chengxiao on 2016/12/8.
 */
public class MergeSort {
    public static void main(String []args) {
        int []arr = {9,8,7,6,5,4,3,2,1};
        sort(arr);
        System.out.println(Arrays.toString(arr));
    }
    
    public static void sort(int []arr) {
    //在排序前,先建好一个长度等于原数组长度的临时数组,避免递归中频繁开辟空间
        int []temp = new int[arr.length];
        sort(arr,0,arr.length-1,temp);
    }
    
    private static void sort(int[] arr, int left, int right, int []temp) {
        if (left < right) {
            int mid = (left+right)/2;
            sort(arr,left,mid,temp);		//左边归并排序,使得左子序列有序
            sort(arr,mid+1,right,temp);		//右边归并排序,使得右子序列有序
            merge(arr,left,mid,right,temp); //将两个有序子数组合并操作
        }
    }
    
    private static void merge(int[] arr, int left, int mid, int right, int[] temp) {
        int i = left;		//左序列指针
        int j = mid+1;		//右序列指针
        int t = 0;			//临时数组指针
        while (i <= mid && j <= right) {
            if (arr[i] <= arr[j]) {
                temp[t++] = arr[i++];
            } else {
                temp[t++] = arr[j++];
            }
        }

		//将左边剩余元素填充进temp中
        while (i <= mid) {
            temp[t++] = arr[i++];
        }

		//将右序列剩余元素填充进temp中
        while (j <= right) {
            temp[t++] = arr[j++];
        }
        t = 0;
        
        //将temp中的元素全部拷贝到原数组中
        while(left <= right){
            arr[left++] = temp[t++];
        }
    }
}

7. 计数排序(非稳定)

  • 把每个出现的数值都做一个计数,然后根据计数从小到大输出得到有序数组。
  • 保持稳定性的做法是:先对计数数组做前缀和,在第 2 步往回赋值的时候,根据原始输入数组的数据从后向前赋值,前缀和数组保存了每个元素存放的下标信息
public class Solution {

    // 计数排序

    private static final int OFFSET = 50000;

    public int[] sortArray(int[] nums) {
        int len = nums.length;
        // 由于 -50000 <= A[i] <= 50000
        // 因此"桶" 的大小为 50000 - (-50000) = 10_0000
        // 并且设置偏移 OFFSET = 50000,目的是让每一个数都能够大于等于 0
        // 这样就可以作为 count 数组的下标,查询这个数的计数
        int size = 10_0000;

        // 计数数组
        int[] count = new int[size];
        // 计算计数数组
        for (int num : nums) {
            count[num + OFFSET]++;
        }

        // 把 count 数组变成前缀和数组
        for (int i = 1; i < size; i++) {
            count[i] += count[i - 1];
        }

        // 先把原始数组赋值到一个临时数组里,然后回写数据
        int[] temp = new int[len];
        System.arraycopy(nums, 0, temp, 0, len);

        // 为了保证稳定性,从后向前赋值
        for (int i = len - 1; i >= 0; i--) {
            int index = count[temp[i] + OFFSET] - 1;
            nums[index] = temp[i];
            count[temp[i] + OFFSET]--;
        }
        return nums;
    }
}

8. 堆排序

还未深挖:

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;
    }

    /**
     * 将数组整理成堆(堆有序)
     *
     * @param 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;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Yawn__

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值