归并排序讲解+相关面试题

归并排序整体流程:

在这里插入图片描述
时间复杂度 O(N * logN)

代码实现(java)

包含递归和非递归

public class MergeSort {
    public static void main(String[] args) {
        int[] arr = new int[] {4,1,2,5,7,2,1,7,9,3};
        for (int i : arr) {
            System.out.print(i + " ");
        }
        mergeSort2(arr);
        System.out.println();
        for (int i : arr) {
            System.out.print(i + " ");
        }
    }

    /**
     * 递归方式的归并排序
     * @param arr
     */
    public static void mergeSort1(int[] arr) {
        if (arr == null) return;
        process(arr, 0, arr.length - 1);
    }

    public static void process(int[] arr, int left, int right) {
        // 只有一个元素,返回
        if (left == right) return;

        int mid = left + (right - left) / 2;

        // 先递归调用process,对数组进行拆分,拆分到只剩下2个元素开始,
        // 一步一步进行排序,然后归并起来在进行进一步的排序,知道整个数组排序好
        process(arr, left, mid);
        process(arr, mid + 1, right);
        // 对当前 left 到 right 进行排序
        merge(arr, left, mid, right);
    }

    /**
     * 非递归的归并排序
     * @param arr
     */
    public static void mergeSort2(int[] arr) {
        if (arr == null || arr.length < 2) return;

        int N = arr.length;
        // 步长,从1开始
        int mergeSize = 1;
        while (mergeSize < N) {
            int L = 0; // 左组的起始位置
            while (L < N) {
                if (mergeSize >= N - L) { // 右组已经不够了,直接跳过
                    break;
                }
                // 左组的终点
                int M = L + mergeSize - 1;
                // 右组的终点, 不是M+步长 就是不够的情况到达数组终点N-1
                int R = M + Math.min(mergeSize, N - M - 1);
                merge(arr, L, M, R);
                L = R + 1;
            }
            // 避免步长越界(数字最大值)
            // 如果下次步长扩大后,已经大于N,就不可能还可以进行merge了,直接跳出即可
            if (mergeSize > N / 2) {
                break;
            }
            mergeSize <<= 1;
        }
    }


    public static void merge(int[] arr, int left, int mid, int right) {
        int[] temp = new int[right - left + 1];
        int i = 0;
        // 前半部分的头
        int L = left;
        // 后半部分的头
        int M = mid + 1;
        // 合并
        while (L <= mid && M <= right) {
            temp[i++] = arr[L] > arr[M] ? arr[M++] : arr[L++];
        }

        while (L <= mid) {
            temp[i++] = arr[L++];
        }
        while (M <= right) {
            temp[i++] = arr[M++];
        }
        // 将排好序的部分复制回原数组
        for (int j = 0; j < temp.length; j++) {
            arr[left + j] = temp[j];
        }
    }
}

面试题

1. 小和问题

在一个数组中,每一个数左边比当前数小的数累加起来,叫做这个数组的小和。求一个数组的小和。

例:[1,3,4,2,5]
1左边比1小的数:0
3左边比3小的数:1
4左边比4小的数:1 + 3 = 4
2左边比2小的数:1
5左边比5小的数:1 + 3 + 4 + 2 = 10
所以小和为1+4+1+10=16

要求时间复杂度为 O(N * logN)

思路

其实就是统计右组中有多少个数大于左组中的较小的数(左组中不产生小和,只有和右组比较时才产生小和,所以统计时不会有重复情况出现,每次都是和最新的右组范围进行比较,也就是每次都和原数组中更靠右的指定范围内的元素进行比较)

  1. 每次进行归并排序的时候,对当前次的排序进行判断
  2. 因为前半部分和后半部分都是有序的,所以判断如果前半部分中的一个数小于后半部分中的某一个数,那么前半部分中这个数,会比后半部分中这个数出现位置及之后的位置都小(有序),所以统计次数并相加到结果中
  3. 最终在递归过程中,将每次归并时统计的和相加,就是小和结果

注意:如果发现相等,则先拷贝右边的数,因为左边的指针不能先动,要先找到右边比左边大的数才能动,这样才能不漏掉小数;否则左边先动,右边可能有比它大的数时,左指针已经移动,会错过

代码实现

public class SmallSum {
    public static int smallSum(int[] arr) {
        if (arr == null || arr.length < 2) {
            return 0;
        }
        return process(arr, 0 , arr.length - 1);
    }

    private static int process(int[] arr, int L, int R) {
        if (L == R) return 0;
        int mid = L + (R - L) / 2;

        return process(arr, L, mid) + process(arr, mid + 1, R) + merge(arr, L, mid, R);
    }

    private static int merge(int[] arr, int l, int mid, int r) {
        int[] help = new int[r - l + 1];
        int i = 0;
        int p1 = l;
        int p2 = mid + 1;
        int res = 0;

        while (p1 <= mid && p2 <= r) {
            res += arr[p1] < arr[p2] ? (r - p2 + 1) * arr[p1] : 0;
            help[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++];
        }

        while (p1 <= mid) {
            help[i++] = arr[p1++];
        }
        while (p2 <= r) {
            help[i++] = arr[p2++];
        }
        for (int j = 0; j < help.length; j++) {
            arr[l + j] = help[j];
        }
        return res;
    }

    public static void main(String[] args) {
        int[] arr = {1,2,3,4};
        System.out.println(process(arr, 0, arr.length - 1));  // 10 = 1 + 3 + 6
    }
}

2. 数组中的逆序对

LeetCode原题

思路

求逆序对,其实就是求一个数前面有几个数是大于这个数的;恰好与小和问题相反

  1. 我们使用归并排序,每次取出左右两部分,然后都从后向前进行遍历判断
  2. 如果左半部分出现大于右半部分的数,说明这个数大于右半部分的对应数及之前的数,所以就可以将右半部分较小的数及之前数的个数累加到结果中(因为左右都有序)
  3. 最终递归的得到个数即可

代码实现

public class ReversePairs {
    public static int reversePairs(int[] nums) {
        if (nums == null || nums.length < 2) {
            return 0;
        }
        return process(nums, 0, nums.length - 1);
    }

    private static int process(int[] nums, int left, int right) {
        if (left == right) return 0;

        int mid = left + (right - left) / 2;

        return process(nums, left, mid) + process(nums, mid + 1, right) + meger(nums, left, mid, right);
    }

    private static int meger(int[] nums, int left, int mid, int right) {
        int[] help = new int[right - left + 1];

        int p1 = mid;
        int p2= right;
        int res = 0;
        int i = help.length - 1;

        while (p1 >= left && p2 > mid) {
            res += nums[p1] > nums[p2] ? p2 - mid : 0;
            help[i--] = nums[p1] > nums[p2] ? nums[p1--] : nums[p2--];
        }

        while (p1 >= left) {
            help[i--] = nums[p1--];
        }
        while (p2 > mid) {
            help[i--] = nums[p2--];
        }
        for (int j = 0; j < help.length; j++) {
            nums[left + j] = help[j];
        }
        return res;
    }

    public static void main(String[] args) {
        int[] arr = {7,5,6,4};
        System.out.println(process(arr, 0 , arr.length - 1));
    }
}

3. 数组中一个数num的右边有多少数*2后依然小于num

思路

每次归并排序,遍历左半部分,找右半部分不能满足题目要求的位置,该位置之前的数都满足,累加到结果中

代码实现

public class BiggerThanRightTwice {
    public static int biggerTwice(int[] arr) {
        if (arr == null || arr.length < 2) {
            return 0;
        }
        return process(arr, 0, arr.length - 1);
    }

    public static int process(int[] arr, int l, int r) {
        if (l == r) {
            return 0;
        }
        // l < r
        int mid = l + ((r - l) >> 1);
        return process(arr, l, mid) + process(arr, mid + 1, r) + merge(arr, l, mid, r);
    }

    private static int merge(int[] arr, int left, int mid, int right) {
        int res = 0;

        // 先计算
        // 目前囊括进来的数,是从[M+1, windowR),windowR取不到,不被包括结果
        int windowR = mid + 1;
        for (int i = left; i <= mid; i++) {
            while (windowR <= right && arr[i] > (arr[windowR] * 2)) {
                windowR++;
            }
            res += windowR - mid - 1;
        }

        // 再排序
        int[] help = new int[right - left + 1];
        int i = 0;
        int p1 = left;
        int p2 = mid + 1;;

        while (p1 <= mid && p2 <= right) {
            help[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++];
        }

        while (p1 <= mid) {
            help[i++] = arr[p1++];
        }
        while (p2 <= right) {
            help[i++] = arr[p2++];
        }
        for (int j = 0; j < help.length; j++) {
            arr[left + j] = help[j];
        }
        return res;
    }

    public static void main(String[] args) {
        int[] arr = {6,7,1,3,2};
        System.out.println(biggerTwice(arr));  // 5 
    }
}

相关补充题目(进阶)

1. LeetCode 327. 区间和的个数

Java基础排序面试中,可能会问到一些经典的排序算法实现以及它们的应用场景,比如数组、链表、字符串等各种数据结构的排序。以下是几个常见的面试题点: 1. **冒泡排序** (Bubble Sort): 简单易懂,适用于小规模数据或已近乎有序的数据。询问如何实现冒泡排序以及其时间复杂度。 2. **选择排序** (Selection Sort): 每次找到最小元素放在已排序部分的末尾,适用于空间复杂度有限的情况。可以提问其实现细节及最坏情况下的性能。 3. **插入排序** (Insertion Sort): 对于小规模或者接近有序的数据效率较高。讲解插入排序的工作原理和优化版本(如二分插入排序)。 4. **快速排序** (Quick Sort): 分治法的经典应用,平均时间复杂度低。可能会涉及递归实现,栈溢出处理以及何时会退化为O(n^2)。 5. **归并排序** (Merge Sort): 稳定排序,常用于外部排序。可以讨论其稳定性和拆分合并的过程。 6. **堆排序** (Heap Sort): 利用堆数据结构实现,适用于大数据量的排序。可能会考察如何建立最大/最小堆。 7. **稳定性** (Stability): 需解释什么是稳定的排序,哪些排序算法是稳定的,哪些不是。 8. **比较次数和交换次数**: 讨论各种排序算法的平均和最坏情况下比较和交换元素的次数。 9. **自适应排序** (Adaptive Sorting): 如计数排序、基数排序等特定类型的问题,适合非整数类型的数据。 10. **排序算法在实际场景中的选择**: 根据数据特点(大小、是否有重复、存储形式等)分析哪种排序算法更合适。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值