数据结构与算法——二分查找,排序

一.二分查找

1.解法1

复制代码
为什么是 i<=j 意味着区间内有未比较的元素, 而不是 i<j ?
   i==j 意味着 i,j 它们指向的元素也会参与比较
   i<j 只意味着 m 指向的元素参与比较
java 复制代码
    public static int binarySearch(int[] a, int target) {
        int i = 0;
        int j = a.length - 1;
        while (i <= j) {
            int m = (i + j) >>> 1;
            if (target < a[m]) {
                j = m - 1;
            } else if (a[m] < target) {
                i = m + 1;
            } else {
                return m;
            }
        }
        return -1;
    }

2.解法2

1.i, m 指针可能是查找目标

2.j 指针不可能是查找目标(这种方法j只是代表一个边界)

3.因为 1. 2. i >= j 时表示区域内没有要找的了

4.改变 i 边界时, m 已经比较过不是目标, 因此需要 i=m+1

5.改变 j 边界时, m 已经比较过不是目标, 同时因为 2. 所以 j=m,怕错过目标值

java 复制代码
    public static int binarySearch(int[] a, int target) {
        int i = 0;
        int j = a.length;
        while (i < j) {
            int m = (i + j) >>> 1;
            if (target < a[m]) {
                j = m;
            } else if (a[m] < target) {
                i = m + 1;
            } else {
                return m;
            }
        }
        return -1;
    }

3.有重复元素时的处理及应用

1.leftmost

能找到就是返回最左侧的值;找不到就返回比目标大的第一个索引位置。

所以就说返回的i就是大于等于目标最靠左的索引位置。

java 复制代码
    public static int leftMost(int[] a, int target) {
        int i = 0;
        int j = a.length - 1;
        while (i <= j) {
            int m = (i + j) >>> 1;
            if (target <= a[m]) {
                j = m - 1;
            } else if (a[m] < target) {
                i = m + 1;
            }
        }
        return i;
    }
搜索插入位置

2.rightmost

i-1返回的是:找的到就是最右边的值;找不到就是比目标小的最右边的值。

java 复制代码
    public static int rightMost(int[] a, int target) {
        int i = 0;
        int j = a.length - 1;
        while (i <= j) {
            int m = (i + j) >>> 1;
            if (target < a[m]) {
                j = m - 1;
            } else if (a[m] <= target) {
                i = m + 1;
            }
        }
        return i - 1;
    }
重复元素的开是位置和结束位置

3.应用

1.求排名(leftmost)
2.求前任(leftmost)
3.范围查询

二.排序

插入排序

从第二个开始不断地向前比较,找到合适的插入位置。排好序的顺序是从前到后逐渐排好的

这里的i可以理解为已排序好的右边界,low是未排序区的左边界,所以我们可以理解a[low]就是我们想要插入的元素,先把它弄一个临时变量存起来,然后它与前面的所有元素依次对比来找到合适的位置插入,插入的位置就是索引i+1的位置,因为i是已排序好的右边界。

java 复制代码
    public static void sort(int[] a) {
        for (int low = 1; low < a.length; low++) {
            int t = a[low];
            int i = low - 1;
            // 自右向左找插入位置,如果比待插入元素大,则不断右移,空出插入位置
            while (i >= 0 && t < a[i]) {
                a[i + 1] = a[i];
                i--;
            }
            // 找到插入位置
            //如果i= low - 1的话,i+1就是low,相当于重复赋值了,没意义。
            //也就是只比前一个元素小,移动了一个位置
            if (i != low - 1) {
                a[i + 1] = t;
            }
        }
    }

希尔排序(插入排序的优化)

可以将插入排序理解为希尔排序的一种特殊情况,也是最终情况,当gap=1时的情况。

只需将上面的代码的1的位置改成gap即可。

java 复制代码
    public static void sort(int[] a) {
        for (int gap = a.length >> 1; gap >= 1; gap = gap >> 1) {
            // gap=4
            for (int low = gap; low < a.length; low++) {
                int t = a[low]; // t=5
                int i = low - gap;
                // 自右向左找插入位置,如果比待插入元素大,则不断右移,空出插入位置
                while (i >= 0 && t < a[i]) {
                    a[i + gap] = a[i];
                    i -= gap;
                }
                // 找到插入位置
                if (i != low - gap) {
                    a[i + gap] = t;
                }
            }
        }
    }

简单选择排序

核心思想是从序列中选择一个最大/最小值,然后确定它的最终位置。

java 复制代码
    public static void sort(int[] a) {
        for (int right = a.length - 1; right > 0; right--) {
            int max = right;
            for (int i = 0; i < right; i++) {
                if (a[i] > a[max]){
                    max = i;
                }
            }
            if (max != right) {
                int t = a[right];
                a[right] = a[max];
                a[max] = t;
            }
        }
    }

    public static void main(String[] args) {
        int[] a = {6, 5, 4, 3, 2, 1};
        System.out.println(Arrays.toString(a));
        sort(a);
        System.out.println(Arrays.toString(a));
    }

代码并不复杂,只是找出最大/最小值,然后把它放在最后/最前的位置即可,但是寻找最大小值的效率很低。下面会推荐一种非常快速的找最大/小值的办法------堆排序

堆排序

1.堆的介绍

堆是一种基于树的数据机构,用完全二叉树实现(除了最后一层都必须填满)

由于它的特性可以用数组来存储元素

2.建堆(建立大顶堆/小顶堆)以及堆的相关方法

与更大的一个孩子进行交换,直至停止。

java 复制代码
public class MaxHeap {
    int[] array;
    int size;

    public MaxHeap(int capacity) {
        this.array = new int[capacity];
    }

    public MaxHeap(int[] array) {
        this.array = array;
        this.size = array.length;
        heapify();
    }

    //建堆
    private void heapify() {
        //找到最后一个非叶子节点,从后往前将所有的非叶子节点依次下潜
        for (int i = size / 2 - 1; i >= 0; i--) {
            down(i);
        }

    }

    //添加元素
    //先添加在尾部,然后逐渐上浮。
    public boolean offer(int offered) {
        if (size == array.length) {
            return false;
        }
        array[size] = offered;
        up(offered, size);
        size++;
        return true;
    }

    //删除堆顶元素
    //替换堆顶和末尾的元素,然后让堆顶元素不断下潜
    public int poll() {
        int top = array[0];
        swap(0, size - 1);
        size--;
        down(0);
        return top;
    }

    //删除指定索引位置的元素
    //替换index处的元素和末尾的元素,然后让index元素不断下潜
    public int poll(int index) {
        if (index > size - 1) {
            return -1;
        }
        int deleted = array[index];
        swap(index, size - 1);
        size--;
        down(index);
        return deleted;
    }

    //取出堆顶元素
    public int peek() {
        int top = array[0];
        return top;
    }


    //替换堆顶元素
    public void replace(int top) {
        array[0] = top;
        down(0);
    }

    //交换
    private void swap(int i, int j) {
        int temp = array[i];
        array[i] = array[j];
        array[j] = temp;
    }

    //下潜
    private void down(int parent) {
        while (true) {
            int max = parent;
            //左孩子
            int left = parent * 2 + 1;
            //右孩子
            int right = left + 1;
            if (left < size && array[max] < array[left]) {
                max = left;
            }
            if (right < size && array[max] < array[right]) {
                max = right;
            }
            //找到了更大了
            if (max == parent) {
                break;
            }
            swap(max, parent);
            parent = max;
        }
    }

    //上浮方法
    private void up(int offered, int index) {
        int child = index;
        while (child > 0) {
            int parent = (child - 1) / 2;
            if (offered > array[parent]) {
                array[child] = array[parent];
                child = parent;
            } else {
                break;
            }
        }
        array[child] = offered;
    }

    public static void main(String[] args) {
        int[] a = {2, 3, 1, 7, 6, 4, 5};
        System.out.println(Arrays.toString(a));
        MaxHeap maxHeap = new MaxHeap(a);
        System.out.println(Arrays.toString(maxHeap.array));
        int peek = maxHeap.peek();
        System.out.println(peek);
        int poll = maxHeap.poll(2);
        System.out.println(Arrays.toString(maxHeap.array));
    }

}

3.堆的力扣题

1.数据流取最大的第K个数
java 复制代码
public class StreamKBigNum {

    private MinHeap minHeap;

    public StreamKBigNum(int k, int[] nums) {
        minHeap = new MinHeap(k);


    }

    public int add(int val) {
        if (!minHeap.isFull()){
            minHeap.offer(val);
        }else if (minHeap.peek() < val){
            minHeap.replace(val);
        }
        return minHeap.peek();
    }

    public static void main(String[] args) {
        StreamKBigNum test = new StreamKBigNum(3, new int[]{});
        System.out.println(test.add(3)); // [3] 3
        System.out.println(test.add(5)); // [3 5] 3
        System.out.println(test.add(10));
        System.out.println(test.add(9));
        System.out.println(test.add(4));
    }
}
2.数据流取数据的中位数
java 复制代码
public class MediumNum {

    //大顶堆
    private PriorityQueue<Integer> left = new PriorityQueue<>(
            (a, b) -> Integer.compare(b, a)
    );
    //小顶堆
    private PriorityQueue<Integer> right = new PriorityQueue<>();

    public void addNum(int num) {
        if (left.size() == right.size()){
            right.offer(num);
            Integer poll = right.poll();
            left.offer(poll);
        }else {
            left.offer(num);
            right.offer(left.poll());
        }
    }

    public double findMedian() {
        if (left.size() == right.size()){
            return (left.peek() + right.peek()) / 2.0;
        }else {
            return left.peek();
        }
    }

    public static void main(String[] args) {
        MediumNum test = new MediumNum();
        test.addNum(1);
        test.addNum(2);
        test.addNum(3);
        test.addNum(7);
        test.addNum(8);
        test.addNum(9);
        System.out.println(test.findMedian());
        test.addNum(10);
        System.out.println(test.findMedian());
        test.addNum(4);
        System.out.println(test.findMedian());
    }
}
3.收集hashmap缓存中出现频率最高的n个字符串
java 复制代码
    public void QueryTopN(List<String> input, List<String> result, int n) {
        // 统计每个字符串出现的次数
        Map<String, Integer> frequencyMap = new HashMap<>();
        for (String str : input) {
            frequencyMap.put(str, frequencyMap.getOrDefault(str, 0) + 1);
        }
        // 小顶堆,用于存储出现次数最多的n个字符串
        PriorityQueue<Map.Entry<String, Integer>> minHeap = new PriorityQueue<>(
                (e1, e2) -> e1.getValue().compareTo(e2.getValue())
        );
        for (Map.Entry<String, Integer> entry : frequencyMap.entrySet()) {
            if (minHeap.size() < n) {
                minHeap.offer(entry);
            } else if (entry.getValue() > minHeap.peek().getValue()) {
                minHeap.poll();
                minHeap.offer(entry);
            }
        }
        // 将堆中的元素取出并反转,得到出现次数从多到少的结果
        List<String> temp = new ArrayList<>();
        while (!minHeap.isEmpty()) {
            temp.add(minHeap.poll().getKey());
        }
        Collections.reverse(temp);
        result.addAll(temp);
    }
4.用大顶堆实现优先级队列
java 复制代码
public class PriorityQueue4<E extends Priority> implements Queue<E> {

    Priority[] array;
    int size;

    public PriorityQueue4(int capacity) {
        array = new Priority[capacity];
    }

    /*
    1. 入堆新元素, 加入到数组末尾 (索引位置 child)
    2. 不断比较新加元素与它父节点(parent)优先级 (上浮)
        - 如果父节点优先级低, 则向下移动, 并找到下一个 parent
        - 直至父节点优先级更高或 child==0 为止
     */
    @Override
    public boolean offer(E offered) {
        if (isFull()) {
            return false;
        }
        int child = size++;
        int parent = (child - 1) / 2;
        while (child > 0 && offered.priority() > array[parent].priority()) {
            array[child] = array[parent];
            child = parent;
            parent = (child - 1) / 2;
        }
        array[child] = offered;
        return true;
    }

    /*
    1. 交换堆顶和尾部元素, 让尾部元素出队
    2. (下潜)
        - 从堆顶开始, 将父元素与两个孩子较大者交换
        - 直到父元素大于两个孩子, 或没有孩子为止
     */
    @Override
    public E poll() {
        if (isEmpty()) {
            return null;
        }
        swap(0, size - 1);
        size--;
        Priority e = array[size];
        array[size] = null; // help GC

        // 下潜
        down(0);

        return (E) e;
    }

    private void down(int parent) {
        int left = 2 * parent + 1;
        int right = left + 1;
        int max = parent; // 假设父元素优先级最高
        if (left < size && array[left].priority() > array[max].priority()) {
            max = left;
        }
        if (right < size && array[right].priority() > array[max].priority()) {
            max = right;
        }
        if (max != parent) { // 有孩子比父亲大
            swap(max, parent);
            down(max);
        }
    }

    private void swap(int i, int j) {
        Priority t = array[i];
        array[i] = array[j];
        array[j] = t;
    }

    @Override
    public E peek() {
        if (isEmpty()) {
            return null;
        }
        return (E) array[0];
    }

    @Override
    public boolean isEmpty() {
        return size == 0;
    }

    @Override
    public boolean isFull() {
        return size == array.length;
    }
}
5.堆排序
java 复制代码
    //堆排序
    public void sort(int[] a) {
        //这里一般还要加一个建堆的函数,因为我们已经在构造函数里加了,这里就没加
        for (int right = size - 1; right > 0; right--) {
            swap(0, right);
            size--;
            down(0);
        }
    }
6.合并多个有序链表

小顶堆的大小就是链表的个数。

1.先将每个链表的头结点放到小顶堆中

2.移除堆顶元素,并将堆顶元素加入到链表中

3.将加入到新链表中元素所在的链表的next节点再加入到小顶堆中,依次循环。

java 复制代码
    public ListNode mergeKLists2(ListNode[] lists) {
        //小顶堆
        PriorityQueue<ListNode> min = new PriorityQueue<>();
        for (ListNode head : lists) {
            if (head != null) {
                min.offer(head);
            }
        }
        //新链表
        ListNode s = new ListNode(-1, null);
        //指针
        ListNode p = s;
        while (!min.isEmpty()) {
            ListNode poll = min.poll();
            p.next = poll;
            p = poll;
            if (poll.next != null) {
                min.offer(poll.next);
            }
        }
        return s.next;
    }


    public static void main(String[] args) {
        ListNode[] lists = {
                ListNode.of(1, 4, 5),
                ListNode.of(1, 3, 4),
                ListNode.of(2, 6),
                null,
        };
        ListNode m = new E01Leetcode23().mergeKLists2(lists);
        System.out.println(m);
    }

冒泡排序

从头开始相邻的两两比对,然后交换,每次都会将最大的元素确定到数组的末尾处,然后不断缩小未排序的边界,逐渐减小。

java 复制代码
    public static void sort(int[] a) {
        int j = a.length - 1;
        while (true) {
            int x = 0;
            for (int i = 0; i < j; i++) {
                if (a[i] > a[i + 1]) {
                    swap(a, i, i + 1);
                    x = i;
                }
            }
            j = x;
            if (j == 0){
                break;
            }
        }
    }

    private static void swap(int[] a, int i, int j) {
        int t = a[i];
        a[i] = a[j];
        a[j] = t;
    }

    public static void main(String[] args) {
        int[] a = {6, 5, 4, 3, 2, 1};
        System.out.println(Arrays.toString(a));
        sort(a);
        System.out.println(Arrays.toString(a));
    }

快排

java 复制代码
public class QuickSelectSort {

    public static void sort(int[] a) {
        quick(a, 0, a.length - 1);
    }

    private static void quick(int[] a, int left, int right) {
        if (left >= right) {
            return;
        }
        int p = partition(a, left, right);
        quick(a, left, p - 1);
        quick(a, p + 1, right);
    }

    private static int partition(int[] a, int left, int right) {
        int idx = ThreadLocalRandom.current().nextInt(right - left + 1) + left;
        // [0~9] right-left+1=3 [0,2]+4=[4,6]
        swap(a, idx, left);
        int pv = a[left];
        int i = left;
        int j = right;
        while (i < j) {
            // 1. j 从右向左找小(等)的
            while (i < j && a[j] > pv) {
                j--;
            }
            // 2. i 从左向右找大的
            while (i < j && a[i] <= pv) {
                i++;
            }
            // 3. 交换位置
            swap(a, i, j);
        }
        swap(a, left, i);
        return i;
    }

    private static void swap(int[] a, int i, int j) {
        int t = a[i];
        a[i] = a[j];
        a[j] = t;
    }

    public static void main(String[] args) {
        int[] a = {9, 3, 7, 2, 8, 5, 1, 4};
        System.out.println(Arrays.toString(a));
        sort(a);
        System.out.println(Arrays.toString(a));
    }
}

相关算法题

1.快速选择算法

基于快排每一次分区就可以确定基准点元素的最终位置,然后再基准点左边的就是比基准点元素小的,右边的就是比基准点元素大的。所以我们想找到数组中第i个元素的位置(从小到大排列),就可以基于快排来做。

java 复制代码
    static int quick(int[] array, int left, int right, int i) {
        /*
            6   5   1   2   [4]

                    2
            1   2   4   6   5

            1   2   4   6   [5]
                        3
            1   2   4   5   6
         */
        int p = partition(array, left, right); // 基准点元素索引值
        if (p == i) {
            return array[p];
        }
        if(i < p) { // 到左边找
            return quick(array, left, p - 1, i);
        } else { // 到右边找
            return quick(array, p + 1, right, i);
        }
    }
2.数组中第k大的元素

这个算法题也是基于上面的思想来的,只不过上面的i指的是索引,而这道算法题找的是第k大,比如说第二大找的就是排在倒数第二索引位置的元素,所以只用改一下i的参数即可,改为a.length - k。

java 复制代码
public int findKthLargest(int[] a, int k) {
    return QuickSelect.quick(
            a, 0, a.length - 1, a.length - k
    );
}
3.数组中中位数
java 复制代码
    public static double findMedian(int[] nums) {
        if (nums.length % 2 == 1) { // 奇数
            return QuickSelect.quick(nums, 0, nums.length - 1, nums.length / 2);
        } else { // 偶数
            int x = QuickSelect.quick(nums, 0, nums.length - 1, nums.length / 2);
            int y = QuickSelect.quick(nums, 0, nums.length - 1, nums.length / 2 - 1);
            return (x + y) / 2.0;
        }
    }

归并排序

要点

分:每次从中间切一刀,处理的数据也就少了一半

治:当切到还只剩下一个数据时,就可以认为是有序的了,因为一个数据本身就有序

合:合并两个有序结果(算法题:合并有序数组)

java 复制代码
    private static void split(int[] a1, int left, int right) {
        // 治:只有一个元素时,已经有序
        if (left >= right) {
            return;
        }

        // 分:找到中点
        int mid = (left + right) >>> 1;  // 等价于 (left + right) / 2,防溢出

        // 递归拆分左半部分 [left, mid]
        split(a1, left, mid);

        // 递归拆分右半部分 [mid+1, right]
        split(a1, mid + 1, right);

        // 合:合并两个有序子数组
        combine(a1, left, mid, right);
    }

    // 合并两个有序区间:[left, mid] 和 [mid+1, right]
    public static void combine(int[] a, int left, int mid, int right) {
        // 临时数组存放合并结果
        int[] temp = new int[right - left + 1];
        int i = left;      // 左子数组起点
        int j = mid + 1;   // 右子数组起点
        int k = 0;         // temp 数组索引

        // 合并两个有序子数组
        while (i <= mid && j <= right) {
            if (a[i] <= a[j]) {
                temp[k++] = a[i++];
            } else {
                temp[k++] = a[j++];
            }
        }

        // 复制剩余元素
        while (i <= mid) {
            temp[k++] = a[i++];
        }
        while (j <= right) {
            temp[k++] = a[j++];
        }

        // 将合并结果拷贝回原数组
        for (int p = 0; p < temp.length; p++) {
            a[left + p] = temp[p];
        }
    }

基数排序

1.代码

java 复制代码
public class RadixSort {
    /*
        110 088 009

        0   088 009
        1   110
        2
        3
        4
        5
        6
        7
        8
        9
        088 009 110 第一轮 重新放回原数组

        0   009
        1   110
        2
        3
        4
        5
        6
        7
        8   088
        9
        009 110 088 第二轮 重新放回原数组

        0   110
        1
        2
        3
        4
        5
        6
        7
        8   088
        9   009
        110 088 009 第三轮 重新放回原数组
     */


    public static void radixSort(String[] a, int length) {
        // 1. 准备桶
        ArrayList<String>[] buckets = new ArrayList[128];
        for (int i = 0; i < buckets.length; i++) {
            buckets[i] = new ArrayList<>();
        }
        // 2. 进行多轮按位桶排序
        for (int i = length - 1; i >= 0; i--) {
            // 将字符串放入合适的桶
            for (String s : a) {
                buckets[s.charAt(i)].add(s);
            }
            // 重新取出排好序的字符串,放回原始数组
            int k = 0;
            for (ArrayList<String> bucket : buckets) {
                for (String s : bucket) {
                    a[k++] = s;
                }
                bucket.clear();
            }
//            System.out.println(Arrays.toString(a));
        }
    }

    public static void main(String[] args) {
        String[] phoneNumbers = new String[10];  // 0~127
        phoneNumbers[0] = "13812345678";  // int long
        phoneNumbers[1] = "13912345678";
        phoneNumbers[2] = "13612345678";
        phoneNumbers[3] = "13712345678";
        phoneNumbers[4] = "13512345678";
        phoneNumbers[5] = "13412345678";
        phoneNumbers[6] = "15012345678";
        phoneNumbers[7] = "15112345678";
        phoneNumbers[8] = "15212345678";
        phoneNumbers[9] = "15712345678";

        /*String[] phoneNumbers = new String[6];
        phoneNumbers[0] = "158";
        phoneNumbers[1] = "151";
        phoneNumbers[2] = "235";
        phoneNumbers[3] = "138";
        phoneNumbers[4] = "139";
        phoneNumbers[5] = "159";*/

        /*
            0
            1   151
            2
            3
            4
            5   135
            6
            7
            8   158 138
            9   139 159
            151 135 158 138 139 159  按个位排

            0
            1
            2
            3   135 138 139
            4
            5   151 158 159
            6
            7
            8
            9
            135 138 139 151 158 159  按十位排
         */

        RadixSort.radixSort(phoneNumbers, 11);
        for (String phoneNumber : phoneNumbers) {
            System.out.println(phoneNumber);
        }
    }
}

处理不同长度的字符串

java 复制代码
    public static void radixSort(String[] a) {
        if (a == null || a.length == 0) return;

        // 1. 找到最长字符串的长度
        int maxLength = 0;
        for (String s : a) {
            if (s.length() > maxLength) {
                maxLength = s.length();
            }
        }

        // 2. 准备桶(ASCII 范围 0-127)
        ArrayList<String>[] buckets = new ArrayList[128];
        for (int i = 0; i < buckets.length; i++) {
            buckets[i] = new ArrayList<>();
        }

        // 3. 从最低位到最高位进行桶排序
        for (int pos = maxLength - 1; pos >= 0; pos--) {
            // 将字符串放入合适的桶
            for (String s : a) {
                // 关键修改:处理不同长度的字符串
                //含义是:如果小于就说明字符串在当前位置是有元素的
                if (pos < s.length()) {
                    // 字符串在当前位置有字符
                    buckets[s.charAt(pos)].add(s);
                } else {
                    // 字符串较短,视为空字符(ASCII 0)
                    buckets[0].add(s);
                }
            }

            // 重新取出排好序的字符串
            int k = 0;
            for (ArrayList<String> bucket : buckets) {
                for (String s : bucket) {
                    a[k++] = s;
                }
                bucket.clear(); // 清空桶,准备下一轮
            }
        }
    }

2.应用场景

相关推荐
漫随流水14 小时前
leetcode算法(145.二叉树的后序遍历)
数据结构·算法·leetcode·二叉树
华如锦14 小时前
四:从零搭建一个RAG
java·开发语言·人工智能·python·机器学习·spring cloud·计算机视觉
Tony_yitao14 小时前
22.华为OD机试真题:数组拼接(Java实现,100分通关)
java·算法·华为od·algorithm
JavaGuru_LiuYu14 小时前
Spring Boot 整合 SSE(Server-Sent Events)
java·spring boot·后端·sse
爬山算法14 小时前
Hibernate(26)什么是Hibernate的透明持久化?
java·后端·hibernate
彭于晏Yan14 小时前
Springboot实现数据脱敏
java·spring boot·后端
漫随流水14 小时前
leetcode算法(94.二叉树的中序遍历)
数据结构·算法·leetcode·二叉树
luming-0215 小时前
java报错解决:sun.net.utils不存
java·经验分享·bug·.net·intellij-idea
北海有初拥15 小时前
Python基础语法万字详解
java·开发语言·python
alonewolf_9915 小时前
Spring IOC容器扩展点全景:深入探索与实践演练
java·后端·spring