大厂算法面试之leetcode精讲13.单调栈

大厂算法面试之leetcode精讲13.单调栈

视频讲解(高效学习):点击学习

目录:

1.开篇介绍

2.时间空间复杂度

3.动态规划

4.贪心

5.二分查找

6.深度优先&广度优先

7.双指针

8.滑动窗口

9.位运算

10.递归&分治

11剪枝&回溯

12.堆

13.单调栈

14.排序算法

15.链表

16.set&map

17.栈

18.队列

19.数组

20.字符串

21.树

22.字典树

23.并查集

24.其他类型题

239. 滑动窗口最大值 (hard)

方法1.优先队列

动画过大,点击查看

  • 思路:最大值问题我们可以采用大顶堆,具体就是维护一个大顶堆,初始的时候将0~k-1的元素加入堆中,存入的是值和索引的键值队,然后滑动窗口从从索引为k的元素开始遍历,将新进入滑动窗口的元素加堆中,当堆顶元素不在滑动窗口中的时候,不断删除堆顶堆元素,直到最大值在滑动窗口里。
  • 复杂度分析:时间复杂度O(nlogn),n是nums的长度,将一个元素加入优先队列的时间复杂度是logn,最坏的情况下所有元素都要入队,所以复杂度是nlogn。空间复杂度是O(n),最坏的情况下,所有元素都在队列中,所以是O(n)

js:

class Heap {
    constructor(comparator = (a, b) => a - b, data = []) {
        this.data = data;
        this.comparator = comparator;//比较器
        this.heapify();//堆化
    }

    heapify() {
        if (this.size() < 2) return;
        for (let i = Math.floor(this.size()/2)-1; i >= 0; i--) {
            this.bubbleDown(i);//bubbleDown操作
        }
    }

    peek() {
        if (this.size() === 0) return null;
        return this.data[0];//查看堆顶
    }

    offer(value) {
        this.data.push(value);//加入数组
        this.bubbleUp(this.size() - 1);//调整加入的元素在小顶堆中的位置
    }

    poll() {
        if (this.size() === 0) {
            return null;
        }
        const result = this.data[0];
        const last = this.data.pop();
        if (this.size() !== 0) {
            this.data[0] = last;//交换第一个元素和最后一个元素
            this.bubbleDown(0);//bubbleDown操作
        }
        return result;
    }

    bubbleUp(index) {
        while (index > 0) {
            const parentIndex = (index - 1) >> 1;//父节点的位置
            //如果当前元素比父节点的元素小,就交换当前节点和父节点的位置
            if (this.comparator(this.data[index], this.data[parentIndex]) < 0) {
                this.swap(index, parentIndex);//交换自己和父节点的位置
                index = parentIndex;//不断向上取父节点进行比较
            } else {
                break;//如果当前元素比父节点的元素大,不需要处理
            }
        }
    }

    bubbleDown(index) {
        const lastIndex = this.size() - 1;//最后一个节点的位置
        while (true) {
            const leftIndex = index * 2 + 1;//左节点的位置
            const rightIndex = index * 2 + 2;//右节点的位置
            let findIndex = index;//bubbleDown节点的位置
            //找出左右节点中value小的节点
            if (
                leftIndex <= lastIndex &&
                this.comparator(this.data[leftIndex], this.data[findIndex]) < 0
            ) {
                findIndex = leftIndex;
            }
            if (
                rightIndex <= lastIndex &&
                this.comparator(this.data[rightIndex], this.data[findIndex]) < 0
            ) {
                findIndex = rightIndex;
            }
            if (index !== findIndex) {
                this.swap(index, findIndex);//交换当前元素和左右节点中value小的
                index = findIndex;
            } else {
                break;
            }
        }
    }

    swap(index1, index2) {//交换堆中两个元素的位置
        [this.data[index1], this.data[index2]] = [this.data[index2], this.data[index1]];
    }

    size() {
        return this.data.length;
    }
}

var maxSlidingWindow = function(nums, k) {
    let ans = [];
    let heap = new Heap((a, b) => b.val - a.val);//大顶堆
    for(let i=0;i<k-1;i++) heap.offer({val: nums[i], index: i});//初始的时候将0~k-1的元素加入堆中
    for(let i=k-1; i<nums.length; i++){//滑动窗口从从索引为k-1的元素开始遍历
        heap.offer({val: nums[i], index: i});//将新进入滑动窗口的元素加堆中
      //当堆顶元素不在滑动窗口中的时候,不断删除堆顶堆元素,直到最大值在滑动窗口里。
        while(heap.peek().index<=i-k) heap.poll();
        ans.push(heap.peek().val);//将在滑动窗口里的最大值加入ans
    }
    return ans;
}

java:

class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
        int n = nums.length;
        PriorityQueue<int[]> pq = new PriorityQueue<int[]>(new Comparator<int[]>() {//大顶堆
            public int compare(int[] pair1, int[] pair2) {
                return pair1[0] != pair2[0] ? pair2[0] - pair1[0] : pair2[1] - pair1[1];
            }
        });
        for (int i = 0; i < k; ++i) {//初始的时候将0~k-1的元素加入堆中
            pq.offer(new int[]{nums[i], i});
        }
        int[] ans = new int[n - k + 1];
        ans[0] = pq.peek()[0];//滑动窗口初始最大值
        for (int i = k; i < n; ++i) {//滑动窗口从从索引为k的元素开始遍历
            pq.offer(new int[]{nums[i], i});//将新进入滑动窗口的元素加堆中
            //当堆顶元素不在滑动窗口中的时候,不断删除堆顶堆元素,直到最大值在滑动窗口里。
            while (pq.peek()[1] <= i - k) {
                pq.poll();
            }
            ans[i - k + 1] = pq.peek()[0];//将在滑动窗口里的最大值加入ans
        }
        return ans;
    }
}
方法2.单调队列

动画过大,点击查看

  • 思路:维护单调递减队列,当进入滑动窗口的元素大于等于队尾的元素时 不断从队尾出队,直到进入滑动窗口的元素小于队尾的元素,才可以入队,以保证单调递减的性质,当队头元素已经在滑动窗口外了,移除对头元素,当i大于等于k-1的时候,单调递减队头就是滑动窗口的最大值
  • 复杂度分析:时间复杂度O(n),n是nums的长度,每个元素入队一次。空间复杂度O(k),队列最多存放k大小的元素

js:

var maxSlidingWindow = function (nums, k) {
    const q = [];//单递减的双端队列
    const ans = [];//最后的返回结果
    for (let i = 0; i < nums.length; i++) {//循环nums
        //当进入滑动窗口的元素大于等于队尾的元素时 不断从队尾出队,
        //直到进入滑动窗口的元素小于队尾的元素,以保证单调递减的性质
        while (q.length && nums[i] >= nums[q[q.length - 1]]) {
            q.pop();
        }
        q.push(i);//元素的索引入队
        while (q[0] <= i - k) {//队头元素已经在滑动窗口外了,移除对头元素
            q.shift();
        }
        //当i大于等于k-1的时候,单调递减队头就是滑动窗口的最大值
        if (i >= k - 1) ans.push(nums[q[0]]);
    }
    return ans;
};

Java:

class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
        int n = nums.length;
        Deque<Integer> deque = new LinkedList<Integer>();
        for (int i = 0; i < k; ++i) {
            while (!deque.isEmpty() && nums[i] >= nums[deque.peekLast()]) {
                deque.pollLast();
            }
            deque.offerLast(i);
        }

        int[] ans = new int[n - k + 1];
        ans[0] = nums[deque.peekFirst()];
        for (int i = k; i < n; ++i) {
            while (!deque.isEmpty() && nums[i] >= nums[deque.peekLast()]) {
                deque.pollLast();
            }
            deque.offerLast(i);
            while (deque.peekFirst() <= i - k) {
                deque.pollFirst();
            }
            ans[i - k + 1] = nums[deque.peekFirst()];
        }
        return ans;
    }
}

84. 柱状图中最大的矩形 (hard)

  • 思路:准备单调递增栈存放数组下标,因为这样可以从栈顶找到左边第一个比自己小的下标,这样从当前下标出发到第一个比自己小的柱子的下标就是矩形面积的宽度,然后在乘当前柱子的高度就是面积,如果当前柱子大于栈顶的下标对应的柱子高度,就入栈,否则不断出栈,计算栈顶的柱子所能形成的矩形面积,然后更新最大矩形面积
  • 复杂度:时间复杂度O(n),n是heights的长度,数组里每个元素尽出栈一次。空间复杂度O(n),栈空间最多为n

动画过大,点击查看

js:

const largestRectangleArea = (heights) => {
    let maxArea = 0
    const stack = [] //单调递增栈 注意栈存的时下标
    heights = [0, ...heights, 0]    //在heights数组前后增加两个哨兵 用来清零单调递增栈里的元素   
    for (let i = 0; i < heights.length; i++) {
        //当前元素对应的高度小于栈顶元素对应的高度时
        while (heights[i] < heights[stack[stack.length - 1]]) {
            const stackTopIndex = stack.pop() //出栈
            maxArea = Math.max(               //计算面积 并更新最大面积
                maxArea,
                heights[stackTopIndex] * (i - stack[stack.length - 1] - 1)//高乘宽
            )
        }
        stack.push(i)//当前下标加入栈
    }
    return maxArea
}

java:

class Solution {
    public int largestRectangleArea(int[] heights) {
        Stack<Integer> stack = new Stack<>();
        int len = heights.length;
        int maxArea = 0;
        for(int i = 0; i < len; i++){
            if(stack.isEmpty() || heights[stack.peek()] <= heights[i])
                stack.push(i);
            else{
                int top = -1;
                while(!stack.isEmpty() && heights[stack.peek()] > heights[i])
                {
                    top = stack.pop();
                    maxArea = Math.max(maxArea, heights[top] * (i - top));
                }

                heights[top] = heights[i];
                stack.push(top);
            }
        }

        while(!stack.isEmpty())
        {
            int top = stack.pop();
            maxArea = Math.max(maxArea, (len - top) * heights[top]);
        }

        return maxArea;
    }
}

85. 最大矩形 (hard)

方法1.单调栈
ds_106
  • 思路:84题的变种,从第一行到第n行形成的柱状图可以利用84题求解,然后循环每一行,计算以这一行为底的柱状图最大面积,然后更新最大矩形面积
  • 复杂度:时间复杂度O(mn),m、n分别是矩形的高度和宽度,循环m次行,每行里循环每个柱子的高度。空间复杂度O(n),heights数组的空间。

js:

var maximalRectangle = function (matrix) {
    if (matrix.length == 0) return 0;

    let res = 0;
    let heights = new Array(matrix[0].length).fill(0);//初始化heights数组
    for (let row = 0; row < matrix.length; row++) {
        for (let col = 0; col < matrix[0].length; col++) {
            if(matrix[row][col] == '1' ) heights[col] += 1;
            else heights[col] = 0;
        }//求出每一层的 heights[] 然后传给84题的函数
        res = Math.max(res, largestRectangleArea(heights));//更新一下最大矩形面积
    }
    return res;
};

const largestRectangleArea = (heights) => {
  let maxArea = 0
  const stack = [] //单调递增栈 注意栈存的时下标
  heights = [0, ...heights, 0]    //在heights数组前后增加两个哨兵 用来清零单调递增栈里的元素   
  for (let i = 0; i < heights.length; i++) { 
    //当前元素对应的高度小于栈顶元素对应的高度时
    while (heights[i] < heights[stack[stack.length - 1]]) { 
      const stackTopIndex = stack.pop() //出栈
      maxArea = Math.max(               //计算面积 并更新最大面积
        maxArea,                        
        heights[stackTopIndex] * (i - stack[stack.length - 1] - 1)//高乘宽
      )
    }
    stack.push(i)//当前下标加入栈
  }
  return maxArea
}

java:

class Solution {
    public int maximalRectangle(char[][] matrix) {
        int row = matrix.length;
        if (row == 0)
            return 0;
        int col = matrix[0].length;
        int[] heights = new int[col + 1];
        heights[col] = -1;
        int res = 0;
        for (int i = 0; i < row; i++) {
            for (int j = 0; j < col; j++) {
                heights[j] = matrix[i][j] == '1' ? heights[j] + matrix[i][j] - '0' : 0;
            }
            res = Math.max(res, largestRectangleArea(Arrays.copyOf(heights, col + 1)));

        }
        return res;
    }

    public int largestRectangleArea(int[] heights) {
        Stack<Integer> stack = new Stack<>();
        int len = heights.length;
        int maxArea = 0;
        for (int i = 0; i < len; i++) {
            if (stack.isEmpty() || heights[stack.peek()] <= heights[i])
                stack.push(i);
            else {
                int top = -1;
                while (!stack.isEmpty() && heights[stack.peek()] > heights[i]) {
                    top = stack.pop();
                    maxArea = Math.max(maxArea, heights[top] * (i - top));
                }

                heights[top] = heights[i];
                stack.push(top);
            }
        }

        return maxArea;
    }
}

496. 下一个更大元素 I (easy)

动画过大,点击查看

  • 思路:
    1. 循环nums2,如果循环的元素大于栈顶元素,并且栈不为空,则不断将栈顶元素作为key,当前元素作为value加入map中
    2. 本质是第一个比栈顶元素大的值会让栈中的元素不断出队 所以这个数就是这些出栈元素的下一个更大的数
    3. 剩下来的元素就是没有找到最大值的
    4. 遍历nums1将结果推入ans中
  • 复杂度:时间复杂度O(m+n),nums1、nums2遍历一遍,nums2中的元素入队出队一次。空间复杂度O(n),栈空间和map的空间的复杂度

js:

let nextGreaterElement = function(nums1, nums2) {
    let map = new Map(), stack = [], ans = [];
  //循环nums2,如果循环的元素大于栈顶元素,并且栈不为空,则不断将栈顶元素作为key,当前元素作为value加入map中
  //本质是第一个比栈顶元素大的值会让栈中的元素不断出队 所以这个数就是这些出栈元素的下一个更大的数
    nums2.forEach(item => {
        while(stack.length && item > stack[stack.length-1]){
            map.set(stack.pop(), item)
        };
        stack.push(item);
    });
    stack.forEach(item => map.set(item, -1));//剩下来的元素就是没有找到最大值的
    nums1.forEach(item => ans.push(map.get(item)));//遍历nums1将结果推入ans中
    return ans;
};

java:

public class Solution {
    public int[] nextGreaterElement(int[] nums1, int[] nums2) {
        int len1 = nums1.length;
        int len2 = nums2.length;
        Deque<Integer> stack = new ArrayDeque<>();
        Map<Integer, Integer> map = new HashMap<>();
        for (int i = 0; i < len2; i++) {
            while (!stack.isEmpty() && stack.peekLast() < nums2[i]) {
                map.put(stack.removeLast(), nums2[i]);
            }
            stack.addLast(nums2[i]);
        }
        int[] ans = new int[len1];
        for (int i = 0; i < len1; i++) {
            ans[i] = map.getOrDefault(nums1[i], -1);
        }
        return ans;
    }
}


42. 接雨水 (hard)

  • 思路:首先考虑暴力做法,找找思路,暴力做法可以遍历数组,在每个位置分别往两边寻找左柱子中的最大高度和右柱子中的最大高度,找到之后,用左右最大高度的较小者减去当前柱子的高度,就是当前位置能接的水量。该方法要循环整个数组,并且每个位置要遍历数组寻找左右柱子高度的最大值,嵌套了一层循环,所以复杂度是O(n^2)

    我们怎样加速嵌套的这层循环呢,其实可以预先计算从左往右和从右往左的最大高度数组,在循环数组的时候,可以直接拿到该位置左右两边的最大高度,当前位置的接水量就是左右两边高度的较小者减去当前位置柱子的高度

  • 复杂度:时间复杂度O(n),寻找左右的最大高度,循环计算每个位置的接水量,总共3个循环,但他们不是嵌套关系。空间复杂度是O(n),n是heights数组,用到了leftMaxrightMax数组,即存放左右两边最大高度的数组。

方法1.动态规划

动画过大,点击查看

js:

var trap = function(height) {
    const n = height.length;
    if (n == 0) {//极端情况
        return 0;
    }

    const leftMax = new Array(n).fill(0);//初始化从左往右看的最大值数组
    leftMax[0] = height[0];
    for (let i = 1; i < n; ++i) {
        leftMax[i] = Math.max(leftMax[i - 1], height[i]);
    }

    const rightMax = new Array(n).fill(0);//初始化从右往左看的最大值数组
    rightMax[n - 1] = height[n - 1];
    for (let i = n - 2; i >= 0; --i) {
        rightMax[i] = Math.max(rightMax[i + 1], height[i]);
    }

    let ans = 0;
    //循环数组,每个位置能接的雨水量就是这个位置左右最大值的较小者减去当前的高度
    for (let i = 0; i < n; ++i) {
        ans += Math.min(leftMax[i], rightMax[i]) - height[i];
    }
    return ans;
};

java:

class Solution {
    public int trap(int[] height) {
        int n = height.length;
        if (n == 0) {
            return 0;
        }

        int[] leftMax = new int[n];
        leftMax[0] = height[0];
        for (int i = 1; i < n; ++i) {
            leftMax[i] = Math.max(leftMax[i - 1], height[i]);
        }

        int[] rightMax = new int[n];
        rightMax[n - 1] = height[n - 1];
        for (int i = n - 2; i >= 0; --i) {
            rightMax[i] = Math.max(rightMax[i + 1], height[i]);
        }

        int ans = 0;
        for (int i = 0; i < n; ++i) {
            ans += Math.min(leftMax[i], rightMax[i]) - height[i];
        }
        return ans;
    }
}
方法2:单调栈

动画过大,点击查看

  • 思路:遍历heights数组,将其中的元素加入单调递减栈,如果当前柱子的高度大于栈顶柱子的高度, 不断出栈,相当于找到左边比当前柱子矮的位置,然后每次出栈之后都要累加一下面积。
  • 复杂度:时间复杂度O(n),n是heights的长度,数组中的每个元素最多入栈出栈一次。空间复杂度O(n),栈的空间,最多不会超过heights的长度

js:

var trap = function(height) {
    let ans = 0;
    const stack = [];//单调递减栈。存放的是下标哦
    const n = height.length;
    for (let i = 0; i < n; ++i) {//循环heights
        //当前柱子的高度大于栈顶柱子的 不断出栈
        while (stack.length && height[i] > height[stack[stack.length - 1]]) {
            const top = stack.pop();
            if (!stack.length) {//栈为空时 跳出循环
                break;
            }
            const left = stack[stack.length - 1];//拿到当前位置左边比当前柱子矮的位置
            const currWidth = i - left - 1;//计算宽度
            const currHeight = Math.min(height[left], height[i]) - height[top];//计算高度
            ans += currWidth * currHeight;//计算当面积
        }
        stack.push(i);//加入栈
    }
    return ans;
};

java:

class Solution {
    public int trap(int[] height) {
        int ans = 0;
        Deque<Integer> stack = new LinkedList<Integer>();
        int n = height.length;
        for (int i = 0; i < n; ++i) {
            while (!stack.isEmpty() && height[i] > height[stack.peek()]) {
                int top = stack.pop();
                if (stack.isEmpty()) {
                    break;
                }
                int left = stack.peek();
                int currWidth = i - left - 1;
                int currHeight = Math.min(height[left], height[i]) - height[top];
                ans += currWidth * currHeight;
            }
            stack.push(i);
        }
        return ans;
    }
}
方法3.双指针

动画过大,点击查看

  • 思路:如果右边存在一个比当前高的柱子,就会形成一个洼地,同理,左边存在一个比当前高柱子,也会形成一个坑,用双指针循环heights数组,判断是否形成洼地,如果能形成洼地,则计算积水量,累加进ans。
  • 复杂度:时间复杂度O(n),n为heights的长度, 总共遍历heights一次。空间复杂度O(1),只用到了两个指针

js:

var trap = function(height) {
    let ans = 0;
    let left = 0, right = height.length - 1;//初始化双指针
    let leftMax = 0, rightMax = 0;//左右两边最大高度
    while (left < right) {//循环双指针
        leftMax = Math.max(leftMax, height[left]);//左边最大值
        rightMax = Math.max(rightMax, height[right]);//右边最大值
        if (height[left] < height[right]) {//右边的柱子高于左边的柱子 计算这个位置的接水量 累加进结果
            ans += leftMax - height[left];
            ++left;
        } else {    //左边的柱子高于或等于右边的柱子 计算这个位置的接水量 累加进结果
            ans += rightMax - height[right];
            --right;
        }
    }
    return ans;
};

java:

class Solution {
    public int trap(int[] height) {
        int ans = 0;
        int left = 0, right = height.length - 1;
        int leftMax = 0, rightMax = 0;
        while (left < right) {
            leftMax = Math.max(leftMax, height[left]);
            rightMax = Math.max(rightMax, height[right]);
            if (height[left] < height[right]) {
                ans += leftMax - height[left];
                ++left;
            } else {
                ans += rightMax - height[right];
                --right;
            }
        }
        return ans;
    }
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容