排序

98 阅读4分钟

排序分类

  • 有什么分类?
    • 内排序:在内存中进行的排序
      • 比较排序
        • 插入排序
          • 直接插入排序
          • 希尔排序
        • 选择排序
          • 直接选择排序
          • 堆排序
        • 交换排序
          • 冒泡排序
          • 快速排序
        • 归并排序
      • 非比较排序
        • 计数排序
        • 基数排序
        • 桶排序
    • 外排序:数据量大,无法把数据全部拷贝到内存,需要使用外存,称为外部排序

插入排序

直接插入排序

  • 稳定性:稳定
  • 平均/最差时间复杂度:o(n^2),元素基本有序时o(n)
  • 空间复杂度:o(1)
  • 原理:每一趟将一个待排记录按其关键字大小插入到已排好序的一段记录的合适的位置上,直到所有待排记录全部插入为止
//直接插入排序
public void insertionSort(int[] nums) {
    for (int i = 0; i < nums.length; i++) {
        int insertNum = nums[i];
        int insertIndex;
        for (insertIndex = i - 1; insertIndex >= 0 && nums[insertIndex] > insertNum; insertIndex--) {
            nums[insertIndex+1]=nums[insertIndex];
        }
        nums[insertIndex+1]=insertNum;
    }
}
//二分优化后的直接插入排序
public void binaryInsertionSort(int[] nums) {
    for (int i = 0; i < nums.length; i++) {
        int insertNum = nums[i];
        int l = 0;
        int r = i - 1;
        while (l < r) {
            int mid = l + (r - l) / 2;
            if (insertNum < nums[mid])
                r = mid;
            else l = mid + 1;
        }
        if (i - l >= 0) {
            System.arraycopy(nums, l, nums, l + 1, i - l);
            nums[l] = insertNum;
        }
    }
}

希尔排序

  • 稳定性:不稳定
  • 平均/最差时间复杂度:o(n^1.3)/o(n^2),最优时间复杂度o(n)
  • 空间复杂度:o(1)
  • 原理:对记录按下标的一定增量分组,对每组进行直接插入排序,每次排序后减少增量,增量减至1排序完成(直接插入的排序只是设置增量为1的增量排序,可以理解为特殊的希尔排序)
public void shellSort(int[] nums) {
    for (int d = nums.length / 2; d > 0 ; d /= 2) {
        //直接插入排序
        for (int i = d; i < nums.length; i++) {
            int insertNum = nums[i];
            int insertIndex;
            for (insertIndex = i - d; insertIndex >= 0 && nums[insertIndex] > insertNum; insertIndex -= d) {
                nums[insertIndex + d] = nums[insertIndex];
            }
            nums[insertIndex + d] = insertNum;
        }
    }
}

选择排序

直接选择排序

  • 稳定性:不稳定
  • 平均/最差时间复杂度:o(n^2)
  • 空间复杂度:o(1)
  • 原理:每次在未排序序列中选出最小值,和未排序序列中的第一个元素交换,然后在剩余未排序序列重复该操作直至有序
//直接选择排序
public void selectSort(int[] nums) {
    int minIndex;
    for (int index = 0; index < nums.length - 1; index++) {
        minIndex = index;
        for (int i = index + 1; i < nums.length; i++) {
            if (nums[i] < nums[minIndex])
                minIndex = i;
        }
        if (index != minIndex) {
            swap(nums, index, minIndex);
        }
    }
}

堆排序

  • 稳定性:不稳定
  • 平均/最差时间复杂度:o(nlogn)
  • 空间复杂度:o(1)
  • 原理:将待排序记录看作完全二叉树,可以建立大根堆或小根堆,大根堆中每个节点的值都不小于它的子节点值,小根堆中每个节点的值都不大于它的子节点值。
    • 以大根堆为例,在建堆时首先将最后一个节点作为当前节点,如果当前节点存在父节点且值大于父节点,就将当前节点和父节点交换。在移除时首先暂存根节点的值,然后用最后一个节点代替根节点并作为当前节点,如果当前节点存在子节点且值小于子节点,就将其与值较大的子节点进行交换,调整完堆后返回暂存的值。
//堆排序
public void heapSort(int[] nums) {
    //1.构建大顶堆
    for (int i = nums.length / 2 - 1; i >= 0; i--) {
        //从第一个非叶子结点从下至上,从右至左调整结构
        adjustHeap(nums, i, nums.length);
    }
    //2.调整堆结构+交换堆顶元素与末尾元素
    for (int j = nums.length - 1; j > 0; j--) {
        swap(nums, 0, j);//将堆顶元素与末尾元素进行交换
        adjustHeap(nums, 0, j);//重新对堆进行调整
    }
}

/**
 * 调整大顶堆(仅是调整过程,建立在大顶堆已构建的基础上)
 *
 * @param nums
 * @param i
 * @param length
 */
public static void adjustHeap(int[] nums, int i, int length) {
    int temp = nums[i];//先取出当前元素i
    for (int k = i * 2 + 1; k < length; k = k * 2 + 1) {//从i结点的左子结点开始,也就是2i+1处开始
        if (k + 1 < length && nums[k] < nums[k + 1]) {//如果左子结点小于右子结点,k指向右子结点
            k++;
        }
        if (nums[k] > temp) {//如果子节点大于父节点,将子节点值赋给父节点(不用进行交换)
            nums[i] = nums[k];
            i = k;
        } else {
            break;
        }
    }
    nums[i] = temp;//将temp值放到最终的位置
}

交换排序

冒泡排序

  • 稳定性:稳定
  • 平均/最差时间复杂度:都为o(n^2),数组基本有序时时间复杂度o(n)
  • 空间复杂度:o(1)
  • 原理:比较相邻元素,如果第一个比第二个大就进行交换,对每一对相邻元素做相同的操作,从开始一对到末尾一对,每一轮下来末尾都是有序的,针对n个元素重复以上步骤n-1次
//冒泡排序
public void bubbleSort(int[] nums) {
    for (int i = 0; i < nums.length - 1; i++) {
        for (int index = 0; index < nums.length - 1 - i; index++) {
            if (nums[index] > nums[index + 1])
                swap(nums, index, index + 1);
        }
    }
}

//优化的冒泡排序,设置一个标志位,如果没有元素交换,说明数组已经有序,直接退出
public void betterBubbleSort(int[] nums) {
    boolean swap;
    for (int i = 0; i < nums.length - 1; i++) {
        swap = true;
        for (int index = 0; index < nums.length - 1 - i; index++) {
            if (nums[index] > nums[index + 1]) {
                swap(nums, index ,index + 1);
                swap = false;
            }
        }
        if (swap) break;
    }
}

快速排序

  • 稳定性:不稳定
  • 平均/最差时间复杂度:都为o(nlogn),数组基本有序时时间复杂度o(n^2)
  • 空间复杂度:o(logn)
  • 原理:选定一个基准,通过一次排序将数据分割成两个部分,一部分小于等于基准,一部分大于等于基准,然后按照此法递归这两部分数据排序
  • 优化: 当规模足够小时,例如 end - start < 10 时,采用直接插入排序。
//快速排序
//递归版本
private static void QSort(int[] nums,int l,int r){
    //判断是不是仅有一个元素
    if(l<r){
        int mid=part(nums,l,r);
        QSort(nums,l,mid-1);
        QSort(nums,mid+1,r);
    }
}
//非递归版本
private static void QSortByStack(int[] nums,int l,int r){
    LinkedList<Integer> stack=new LinkedList<>();
    int mid=part(nums,l,r);
    //判断右半部分是否仅有一个数据 
    //将边界入栈,需要注意左右部分都先压左边界或右边界。顺序需要相同,以防出栈时不好判断是low还是high,此方法先压左边界后压右边界
    if(mid+1<r){
        stack.push(mid+1);
        stack.push(r);
    }
    //判断左半部分是否仅有一个数据
    if(mid-1>l){
        stack.push(l);;
        stack.push(mid-1);
    }
    while(!stack.isEmpty()){
        r=stack.pop();
        l=stack.pop();
        mid=part(nums,l,r);
        if(mid+1<r){
            stack.push(mid+1);
            stack.push(r);
        }
        if(mid-1>l){
            stack.push(l);;
            stack.push(mid-1);
        }
    }
}

private static int part(int[] nums, int l, int r) {
    int privot=nums[l];
    while (l<r){
        while (l<r&&nums[r]>privot)r--;
        nums[l]=nums[r];
        while(l<r&&nums[l]<=privot)l++;
        nums[r]=nums[l];
    }
    nums[l]=privot;
    return l;
}

归并排序

  • 稳定性:稳定
  • 平均/最差时间复杂度:都为o(nlogn)
  • 空间复杂度:o(n)
  • 原理:使用分治的思想对数组分成两个部分,对两个部分分别递归排序,最后合并
    • 使用一个辅助空间,并设两个指针分别指向两个有序数组的开头,将指针指的较小元素加入辅助空间,重复该步骤使得某个指针到达数组的末位。将另一个序列剩余元素全部加入辅助空间
  • 适用场景: 数据量大且对稳定性有要求的情况。
int[] help;

public void mergeSort(int[] arr) {
    int[] help = new int[arr.length];
    sort(arr, 0, arr.length - 1);
}

public void sort(int[] arr, int start, int end) {
    if (start == end) return;
    int mid = start + (end - start) / 2;
    sort(arr, start, mid);
    sort(arr, mid + 1, end);
    merge(arr, start, mid, end);
}

public void merge(int[] arr, int start, int mid, int end) {
    if (end + 1 - start >= 0) System.arraycopy(arr, start, help, start, end + 1 - start);
    int p = start;
    int q = mid + 1;
    int index = start;
    while (p <= mid && q <= end) {
        if (help[p] < help[q])
            arr[index++] = help[p++];
        else
            arr[index++] = help[q++];
    }
    while (p <= mid) arr[index++] = help[p++];
    while (q <= end) arr[index++] = help[q++];
}

如何选择排序算法

  • 数据量规模较小,考虑直接插入或直接选择。当元素分布有序时直接插入将大大减少比较和移动记录的次数,如果不要求稳定性,可以使用直接选择,效率略高于直接插入。

  • 数据量规模中等,选择希尔排序。

  • 数据量规模较大,考虑堆排序(元素分布接近正序或逆序)、快速排序(元素分布随机)和归并排序(稳定性)。

  • 一般不使用冒泡。