【算法】面试中的常见排序算法

22 篇文章 2 订阅

  排序算法不但是算法考试的重点,也是面试中经常会问到的问题。对于常见的算法,不但要能熟练写出,还要明白其时间复杂度、空间复杂度、稳定性以及适用的场景。

1.快速排序
  • 算法思想:选择一个主元,比主元小的放在一边,比主元大的放在另外一边。让后对两边分别递归使用快速排序。

  • 时间复杂度:

    • 平均时间复杂度 O(nlog2n)
    • 最好时间复杂度 O(nlog2n)
    • 最坏时间复杂度 O(n2)
  • 空间复杂度:O(nlog2n)

  • 稳定性:不稳定

  • 适用场景:寻找数组中的第 K 大数。Partition得到 pos 后,若 pos == K - 1,则得到了答案,不用继续寻找。若 pos > K - 1 就到[left, pos - 1] 中寻找,若 pos < K - 1 就到 [pos + 1, right] 中寻找。

  • 不适用场景:元素有序时,退化成冒泡排序,主元不能把当前区间划分成两个长度接近的子区间。

    • 使用随机选择主元可以解决。
    • 三数取中解决:在left、mid、right中选择中间大小的数。
private static void quickSort(int[] nums, int left, int right) {
    if (left < right) {
        int pos = Partition(nums, left, right);
        quickSort(nums, left, pos - 1);
        quickSort(nums, pos + 1, right);
    }
}
private static int Partition(int[] nums, int left, int right) {
    int pos = new Random().nextInt(right - left + 1) + left;
    int pivot = nums[pos];
    nums[pos] = nums[left];
    while (left < right) {
        while (left < right && nums[right] > pivot) right--;
        nums[left] = nums[right];
        while (left < right && nums[left] <= pivot) left++;
        nums[right] = nums[left];
    }
    nums[left] = pivot;
    return left;
}
2.归并排序
  • 算法思想:将初始序列的n个记录看成是n个有序的子序列,每个子序列的长度为1,然后两两归并,得到 n / 2 个长度为2的有序子序列。在此基础上,再对长度为2的有序子序列进行两两归并,得到 n / 4个长度为4的有序子序列。以此类推,直到得到一个长度为n的有序序列。

  • 时间复杂度:

    • 平均时间复杂度 O(nlog2n)
    • 最好时间复杂度 O(nlog2n)
    • 最坏时间复杂度 O(nlog2n)
  • 空间复杂度:O(n)

  • 稳定性:稳定

  • 适用场景:

    • n 较大且要求是稳定的排序。
    • 对链表排序。
  • 不适用场景:要求原地排序。

// 递归版本
public void mergeSort(int[] nums, int left, int right) {
    if (left < right) {
        // >>>1相当于对(left+right)/2,但不会溢出
        int mid = (left + right) >>> 1;
        mergeSort(nums, left, mid);
        mergeSort(nums, mid + 1, right);
        merge(nums, left, mid, mid + 1, right);
    }
}
// 非递归版本
public void mergeSort2(int[] nums) {
    int n = nums.length;
    for (int step = 2; step / 2 <= n; step <<= 1) {
        for (int i = 0; i < n; i+=step) {
            int mid = i + step / 2 - 1;
            if (mid + 1 < n) {
                merge(nums, i, mid, mid + 1, Math.min(i + step - 1, n - 1));
            }
        }
    }
}

public void merge(int[] nums, int L1, int R1, int L2, int R2) {
    int i = L1, j = L2;
    int[] temp = new int[R2 - L1 + 1];
    int idx = 0;
    while (i <= R1 && j <= R2) {
        if (nums[i] <= nums[j]) temp[idx++] = nums[i++];
        else temp[idx++] = nums[j++];
    }
    while (i <= R1) temp[idx++] = nums[i++];
    while (j <= R2) temp[idx++] = nums[j++];
    for (i = 0; i < idx; i++) {
        nums[i + L1] = temp[i];
    }
}
3.堆排序
  • 算法思想:堆是一棵完全二叉树,树中的每个结点的值都不小于(或不大于)其左右孩子结点的值。最大元素在对堆顶是大顶堆,反之是小顶堆。一般用优先队列实现。

  • 时间复杂度:

    • 平均时间复杂度 O(nlog2n)
    • 最好时间复杂度 O(nlog2n)
    • 最坏时间复杂度 O(nlog2n)
  • 空间复杂度:O(1)

  • 稳定性:不稳定

  • 适用场景:

    • 寻找 topN。建立一个小顶堆,遍历数组,大于堆顶元素才插入,插入后如果堆的 size > N 那么弹出堆顶元素。最终堆里面存储的就是 topN 的元素。时间复杂度为M*log(N)
    • 维护数据流中的中位数。建立一个大顶堆存较小元素,小顶堆存较大元素。保持大顶堆和小顶堆 size 相等或者大顶堆的size 等于小顶堆 size + 1。大顶堆堆顶元素或者两个堆堆顶元素就是中位数。
// 方法一:用有序队列实现(推荐)
import java.util.Comparator;
import java.util.PriorityQueue;

public class Main {
    public static void main(String[] args) {
        int[] nums = new int[]{1, 5, 6, 6, 3, 1, 2};
        PriorityQueue<Integer> minHeap = new PriorityQueue<>();
        PriorityQueue<Integer> maxHeap = new PriorityQueue<>(new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                return o2.compareTo(o1);
            }
        });
        for (int num : nums) {
            minHeap.offer(num);
            maxHeap.offer(num);
        }
        while (!minHeap.isEmpty()) {
            System.out.print(minHeap.poll() + " ");
        }
        System.out.println();
        while (!maxHeap.isEmpty()) {
            System.out.print(maxHeap.poll() + " ");
        }
    }
}

// 方法二:用数组实现
public class Main {
    private static int maxN;    // 堆中最多能容纳多少个元素(数组的大小)
    private static int n;  // 元素个数
    private static int[] heap;  // 堆

    /* 向下调整,时间复杂度O(logn)
     * 将当前节点与左右孩子比较,与值更大的交换
     */
    public static void downAdjust(int low, int high) {
        int i = low, j = i * 2; // i 为当前节点,j 是其左孩子
        while (j <= high) {
            // 如果有孩子存在且大于左孩子,那么移动到右孩子
            if (j + 1 <= high && heap[j + 1] > heap[j]) {
                j = j + 1;
            }
            // 交换并向下移动
            if (heap[j] > heap[i]) {
                swap(i, j);
                i = j;
                j = i * 2;
            } else break;   // 孩子的权值均比欲调整节点 i 小,调整结束
        }
    }

    /* 建堆。从 n/2个节点开始倒着枚举。
     * 时间复杂度O(n)
     */
    public static void createHeap() {
        for (int i = n / 2; i >= 1; i--) {
            downAdjust(i, n);
        }
    }

    /* 删除堆顶元素:用最后一个元素覆盖堆顶元素并向下调整。
     * 时间复杂度O(logn)
     */
    public static void deleteTop() {
        heap[1] = heap[n--];
        downAdjust(1, n);
    }

    /* 添加元素:放到数组最后,然后向上调整。
     * 时间复杂度 O(logn)
     */
    public static void upAdjust(int low, int high) {
        int i = high, j = i / 2;
        while (j >= low) {
            if (heap[j] < heap[i]) {
                swap(i, j);
                i = j;
                j = i / 2;
            } else break;
        }
    }
    public static void insert(int x) {
        heap[++n] = x;
        upAdjust(1, n);
    }

    /* 堆排序 */
    public static void heapSort() {
        createHeap();
        for (int i = n; i > 1; i--) {   // 倒着枚举,不断减小堆的元素个数
            swap(i, 1);     // 将堆顶元素放到最后一个位置
            downAdjust(1, i - 1);   // 重新调整堆
        }
    }

    public static void swap(int i, int j) {
        int temp = heap[i];
        heap[i] = heap[j];
        heap[j] = temp;
    }
    public static void main(String[] args) {
        maxN = 100;
        heap = new int[maxN];

        int[] input = new int[]{5, 6, 1, 3, 6, 2, 1};
        n = input.length;
        for (int i = 1; i <= n; i++) {
            heap[i] = input[i - 1];
        }

        heapSort();
        for (int i = 1; i <= n; i++) {
            System.out.print(heap[i] + " ");
        }
    }
}
4.直接插入排序
  • 算法思想:对于 nums[1]~nums[n],从 i = 2 开始枚举,每次开始,nums[1]~nums[i - 1] 已经有序,[i, n] 无序。每趟排序从 [1, i - 1] 中找到一个位置 j,使得 nums[i] 放到 j,原本的 nums[j]~nums[i - 1] 后移一位之后,[1, i]变成有序。

  • 时间复杂度:

    • 平均时间复杂度 O(n2)
    • 最好时间复杂度 O(n)
    • 最坏时间复杂度 O(n2)
  • 空间复杂度:O(1)

  • 稳定性:稳定

  • 适用场景:基本有序时使用。最好时间复杂度为 O(n)。

  • 不适用场景:数据量庞大的时候,插入后的数据移动很多。

public void insertSort(int[] nums) {
    for (int i = 1; i < n; i++){	// 进行 n-1 趟排序
        int temp = nums[i], j = i;
        while (j > 0 && temp < nums[j - 1]) {
            nums[j] = nums[j - 1];
            j--;
        }
        nums[j] = temp;
    }
}
5.冒泡排序
  • 算法思想:本质是交换,每次把当前剩余的所有元素中最大的元素移到最后。剩余元素为 0 时排序结束。

  • 时间复杂度:

    • 平均时间复杂度 O(n2)
    • 最好时间复杂度 O(n)
    • 最坏时间复杂度 O(n2)
  • 空间复杂度:O(1)

  • 稳定性:稳定

  • 适用场景:基本有序时使用。设一个标志位,没有发生移动就提前结束排序。

public void sortArray(int[] nums) {
    int len = nums.length;
    for (int i = len - 1; i >= 0; i--) {
        // 先默认数组是有序的,只要发生一次交换,就必须进行下一轮比较,
        // 如果在内层循环中,都没有执行一次交换操作,说明此时数组已经是升序数组
        boolean sorted = true;
        for (int j = 0; j < i; j++) {
            if (nums[j] > nums[j + 1]) {
                int temp = nums[j];
                nums[j] = nums[j + 1];
                nums[j + 1] = temp;
                sorted = false;
            }
        }
        if (sorted) break;
    }
}
6.简单选择排序
  • 算法思想:每次从 nums[i]~nums[n] 中选择最小的与待排序部分的第一个元素 nums[i] 交换,这样有序区间从 [1, i - 1] 变成了 [1, i]。重复这样的操作直到全部有序。

  • 时间复杂度:

    • 平均时间复杂度 O(n2)
    • 最好时间复杂度 O(n2)
    • 最坏时间复杂度 O(n2)
  • 空间复杂度:O(1)

  • 稳定性:不稳定

public void selectSort(int[] nums) {
    for (int i = 0; i < n; i++) {	// 进行 n 趟操作
        int k = i;
        for (int j = i; j < n; j++) {	// 选择[i, n)中的最小值
            if (nums[j] < nums[k]) k = j;
        }
        int temp = nums[k];
        nums[k] = nums[i];
        nums[i] = temp;
    }
}

欢迎关注公众号!
在这里插入图片描述

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值