LeetCode刷题模板(1):《我要打10个》之二分法

Author       :  叨陪鲤

Email         : vip_13031075266@163.com

Date          : 2021.01.23

Copyright : 未经同意不得转载!!!

Version    : 第一章 二分法

ReferenceLeetCode刷题笔记之模板整理》


目录

1. 二分法

1.1 什么是二分查找

1.2 如何识别二分法

1.3 二分法模板

1.3.1 模板一

1.3.2 Lc69:x的平方根

1.3.3 Lc374:猜数大小

1.3.4 Lc33:搜索旋转数组

1.3.5 模板二

1.3.6 Lc278:第一个错误版本

1.3.7 Lc162:寻找峰值

1.3.8 Lc153:寻找旋转排序数组最小值

1.3.9 Lc154:寻找旋转排序数组最小值II

1.3.10  模板三

 1.3.11 LC-34:在排序数组中查找元素的第一个和最后一个

1.3.12 LC-658:找到K个最接近的元素

1.4 小结


1. 二分法

1.1 什么是二分查找

    二分查找也称折半查找(Binary Search),它是一种效率较高的查找方法,前提是数据结构必须先排好序,可以在数据规模的对数时间复杂度内完成查找。但是,二分查找要求线性表具有有随机访问的特点(例如数组),也要求线性表能够根据中间元素的特点推测它两侧元素的性质,以达到缩减问题规模的效果

    二分查找是计算机科学中最基本、最有用的算法之一。 它描述了在有序集合中搜索特定值的过程

二分查找中的经常使用的术语:

  • 目标 Target:你要查找的值
  • 索引 Index:你要查找的当前位置
  • 左右指示符 Left,Right:用来确定查询空间范围的指标
  • 中间指示符 Mid:用来确定接下来在左侧查找还是在右侧查找

1.2 如何识别二分法

   二分查找是一种在每次查询比较之后,将查找空间一分为二的算法,查询时间复杂度通常为O(log2n)。相对于一般遍历算法性能高出很多,因此每次需要查找集合中的索引或者元素时,都应该考虑二分查找。如果集合原本是无序的,我们需要先进行排序然后再二分查找。

二分查找一般有三个主要部分组成:

  • 预处理

如果待查找集合是未排序的,首先需要对其进行排序

  • 二分查找

    用循环或者递归在每一次比较之后,将查询空间一分为二

  • 后处理

    在剩余空间中确定可行的候选者

1.3 二分法模板

    当我们第一次学会二分查找时,我们可能会挣扎。我们可能会在网上研究数百个二分查找问题,每次我们查看开发人员的代码时,它的实现似乎都略有不同。尽管每个实现在每个步骤中都会将问题空间划分为原来的 1/2,但其中有许多问题:

  • 为什么执行方式略有不同?
  • 开发人员在想什么?
  • 哪种方法更容易?
  • 哪种方法更好?

    经过许多次失败的尝试并拉扯掉大量的头发后,我们找到了三个主要的二分查找模板。为了防止脱发,并使新的开发人员更容易学习和理解,我们在接下来一一介绍他们。

 

1.3.1 模板一

func binarySearch(nums []int, target int) int {
    if nums == nil {
        return -1
    }

    left, right := 0, len(nums)-1
    for left <= right {
        mid := left + (right-left)>>1 
        if nums[mid] == target {  
            return mid
        } else if nums[mid] < target { 
            left = mid + 1
        } else if nums[mid] > target {
            right = mid - 1
        }
    }
    //End Condition: left > right
    return -1
}

此模板是二分查找最基础和最基本的形式,是一个标准的二分查找模板,要求背诵全文

这里说明下mid的计算:

通常情况下mid = (left + right)/2。但是这存在溢出的可能性:

比如说left,right,mid都是uint8(一个字节),

left=100,

right=250,

left + right= 350

我们想要的是(left+ right)/2 = 175,但由于350这已经超过uint8的范围了,最后算出来的结果为47,溢出的情况下算出来的值是不准确。而使用下面的方法则不存在此问题,始终可以保证left <= mid <= right。

除此之外由于移位运算的性能高于乘除运算,因此这里我采用了右移来代替除以2的操作。

mid := left + (right-left)>>1 
  • 二分查找最基础和最基本的形式
  • 查找添加可以在不与元素两侧进行比较的情况下确定
  • 无需后处理过程,因为在每一步中都在检查是否找到元素。因此如果程序到达末尾,则说明未找到该元素。
  • 初始条件: ```left=0, right=length-1```
  • 终止    : ```left > right```
  • 向左查找: ```right = mid – 1```
  • 向右查找: ```left = mid + 1````

1.3.2 Lc69:x的平方根

原题链接

  • 题目描述

实现 int sqrt(int x) 函数。

计算并返回 x 的平方根,其中 x 是非负整数。

由于返回类型是整数,结果只保留整数的部分,小数部分将被舍去。

  • 示例

示例 1:

    输入: 4

    输出: 2

示例2:

    输入: 8

    输出: 2

    说明: 8 的平方根是 2.82842..., 由于返回类型是整数,小数部分将被舍去。

  • 实现

Leetcode题的解法有很多中,比如该题Leetcode官方便给出了三种解决方法,二分法求解只是其中的一种,当然可能不是最优的解法(该题比较牛逼的解法是袖珍计算器法,将开方运算转换为指数+对数运算,数学上很强),但由于我们是为了学习二分法,因此这里使用二分法来求解。

func mySqrt(x int) int {
    if x < 0 {
        return -1
    }
    if x == 0 {
        return 0
    }

    left, right := 1, x
    for left <= right {
        mid := left + (right-left)>>1

        val := mid * mid
        if val < x {
            left = mid + 1
        } else if val > x {
            right = mid - 1
        } else {
            return mid
        }
    }
    return right
}

关于为什么最后返回right,这里使用一张图进行说明:

  • 复杂度分析

时间复杂度:O(log x),即为二分查找需要的次数。

空间复杂度:O(1)

 

1.3.3 Lc374:猜数大小

原题链接

  • 题目描述

规则如下:

  1. 每轮游戏,我都会从1到n随机选择一个数字。 请你猜选出的是哪个数字。
  2. 如果你猜错了,我会告诉你,你猜测的数字比我选出的数字是大了还是小了。

你可以通过调用一个预先定义好的接口 int guess(int num) 来获取猜测结果,返回值一共有 3 种可能的情况(-1,1 或 0):

  • -1:我选出的数字比你猜的数字小 pick < num
  • 1 :我选出的数字比你猜的数字大 pick > num
  • 0 :我选出的数字和你猜的数字一样。恭喜!你猜对了!pick == num

返回我选出的数字。

  • 示例
示例 1:
输入:n = 10, pick = 6
输出:6

示例 2:
输入:n = 1, pick = 1
输出:1

示例 3:
输入:n = 2, pick = 1
输出:1

示例 4:
输入:n = 2, pick = 2
输出:2
  • 实现
func guessNumber(n int) int {
    left, right := 1, n
    for left <= right {
        mid := left + (right-left)>>1
        ret := guess(mid)

        if ret == 1 {
            left = mid + 1
        } else if ret == -1 {
            right = mid - 1
        } else {
            return mid
        }
    }
}
  • 复杂度分析

时间复杂度:O(log n),即为二分查找需要的次数。

空间复杂度:O(1)

1.3.4 Lc33:搜索旋转数组

原题链接

  • 题目描述

整数数组nums按升序排列,数组中的值互不相同

在传递给函数之前,nums 在预先未知的某个下标 k(0 <= k < nums.length)上进行了旋转,使数组变为 [nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]](下标从0开始计数)。例如, [0,1,2,4,5,6,7] 在下标3处经旋转后可能变为[4,5,6,7,0,1,2] 。

给你旋转后的数组nums和一个整数target,如果nums中存在这个目标值 target ,则返回它的索引,否则返回 -1 。

  • 示例
示例 1:
    输入:nums = [4,5,6,7,0,1,2], target = 0
    输出:4

示例 2:
    输入:nums = [4,5,6,7,0,1,2], target = 3
    输出:-1

示例 3:
    输入:nums = [1], target = 0
    输出:-1
  • 实现

要点:如果首先找到mid所在的左侧有序区间还是在右侧有序区间,在确定区间之后,根据target和mid的关系(直接判断有序序列,如果不在有序序列范围内,那只能在另一半空间)。

判断mid所在序列比较简单:

如果nums[mid] > nums[0], 则在左侧序列中

如果nums[mid] < nums[0], 则在右侧序列中

func search(nums []int, target int) int {
    if nums == nil {
        return -1
    }
    left, right := 0, len(nums)-1
    for left <= right {
        mid := left + (right-left)/2
        if nums[mid] == target {
            return mid
        }
        if nums[mid] >= nums[left] { //mid在左侧序列中
            if nums[mid] > target && target >= nums[left] {
                right = mid - 1
            } else {
                left = mid + 1
            }
        } else{ //mid在右侧序列中
            if nums[mid] < target && target <= nums[right] {
                left = mid + 1
            } else {
                right = mid - 1
            }
        }
    }
    return -1
}
  • 复杂度分析

时间复杂度:O(logn)其中 nnums 数组的大小。整个算法时间复杂度即为二分查找的时间复杂度O(logn)

空间复杂度:O(1)我们只需要常数级别的空间存放变量。

 

1.3.5 模板二

func binarySearch2(nums []int, target int) int {
    if nums == nil {
        return -1
    }

    left, right := 0, len(nums)
    for left < right {
        mid := left + (right-left)>>1
        if nums[mid] < target { 
            left = mid + 1
        } else {
            right = mid
        }
    }
    //End Condition: left == right
    if left != len(nums)-1 && nums[left] == target {
        return left
    }
    return left
}

该模板是二分查找的高级模板,它用于查找需要需要访问当前索引以及直接右邻居索引的情况。

举一个例子:在一个排序数字中,查找第一次出现n的索引(也可以极值、最值)。代码中判断条件根据不同情形有不同的形式。

  • 二分查找的高级方法
  • 查找条件需要访问元素的直接右邻居
  • 使用元素的右邻居来确定是否满足条件,并决定是向左还是向右查找
  • 保证查询空间的每一步至少有两个元素(left<right)
  • 需要后续处理过程。因为循环体中每一步至少需要两个元素,因此最终只剩下一个元素时需要单独处理,确定是否满足条件。
  • 初始条件: ```left=0, right=length```
  • 终止    : ```left == right```
  • 向左查找: ```right = mid```
  • 向右查找: ```left = mid + 1````

1.3.6 Lc278:第一个错误版本

原题链接

  • 题目描述

    你是产品经理,目前正在带领一个团队开发新的产品。不幸的是,你的产品的最新版本没有通过质量检测。由于每个版本都是基于之前的版本开发的,所以错误的版本之后的所有版本都是错的。

    假设你有 n 个版本 [1, 2, ..., n],你想找出导致之后所有版本出错的第一个错误的版本。

    你可以通过调用bool isBadVersion(version)接口来判断版本号 version 是否在单元测试中出错。实现一个函数来查找第一个错误的版本。你应该尽量减少对调用 API 的次数。

    简单的概括下:在数组中,第一次出现x的下标

  • 示例
给定 n = 5,并且 version = 4 是第一个错误的版本。

调用 isBadVersion(3)-> false
调用 isBadVersion(5)-> true
调用 isBadVersion(4)-> true

所以,4 是第一个错误的版本。

 

 

 

  • 解法
func firstBadVersion(n int) int {
    if n < 1 {
        return 0
    }
    left, right := 1, n
    for left < right {
        mid := left + (right-left)>>1
        if isBadVersion(mid) {
            right = mid
        } else {
            left = mid + 1
        }
    }
    return right
}
  • 复杂度分析

时间复杂度:O(logn),整个算法时间复杂度即为二分查找的时间复杂度O(logn)

空间复杂度:O(1)

1.3.7 Lc162:寻找峰值

题目链接

  • 题目描述

    峰值元素是指其值大于左右相邻值的元素。给你一个输入数组 nums,找到峰值元素并返回其索引。数组可能包含多个峰值,在这种情况下,返回 任何一个峰值 所在位置即可。

    你可以假设 nums[-1] = nums[n] = -∞ 。

  • 示例
示例 1:
    输入:nums = [1,2,3,1]
    输出:2
    解释:3 是峰值元素,你的函数应该返回其索引 2。

示例 2:
    输入:nums = [1,2,1,3,5,6,4]
    输出:1 或 5 
    解释:你的函数可以返回索引 1,其峰值元素为 2;或者返回索引 5, 其峰值元素为 6。
  • 代码实现

这道题如果说使用二分法可能没有什么思路,后来看了官方题解,才明白过来。这里做一个说明:

这道题可以分为3中情况分别进行分析:单调递增、单调递减、两个组合。如下图所示

而使用二分法的核心便是确定mid所处的区间:递增区间或者递减区间。如果为递增区间,即mid右面一定有一个峰值;如果在递减区间,则在mid左面一定有一个峰值。(题外话:::忽然想到了高数中罗尔中值定理:闭区间连续,开区间可导,端点值相等,导数为零。而导数为零的点就是其中一个极值点)。为什么这么肯定呢?因为题目中假设nums[-1] = nums[n] = -∞,因此上述结论成立,而二分法要做的就是逐步逼近一个峰值点。

func findPeakElement(nums []int) int {
    if nums == nil {
        return -1
    }
    left, right := 0, len(nums)-1
    for left < right {
        mid := left + (right-left)>>1
        if nums[mid] < nums[mid+1] {
            left = mid + 1
        } else {
            right = mid
        }
    }
    return left //right is also ok
}

这里right=len(nums)-1,并不是模板中的len(nums).除此之外并没有单独判断只剩一个元素的情况。我们还是要使用马克思主义的精髓:具体问题具体分析

  • 复杂度分析

时间复杂度:O(logn)

空间复杂度:O(1)

1.3.8 Lc153:寻找旋转排序数组最小值

题目链接

  • 题目描述

假设按照升序排序的数组在预先未知的某个点上进行了旋转。例如,数组[0,1,2,4,5,6,7] 可能变为[4,5,6,7,0,1,2] 。

请找出其中最小的元素。

提示:

  • 1 <= nums.length <= 5000
  • -5000 <= nums[i] <= 5000
  • nums 中的所有整数都是 唯一 的
  • nums 原来是一个升序排序的数组,但在预先未知的某个点上进行了旋转

 

  • 示例
示例 1:
    输入:nums = [3,4,5,1,2]
    输出:1

示例 2:
    输入:nums = [4,5,6,7,0,1,2]
    输出:0

示例 3:
    输入:nums = [1]
    输出:1
  • 代码实现
func findMin(nums []int) int {
    if nums == nil {
        return 0
    }
    n := len(nums)
    if n == 1 || nums[0] < nums[n-1] {
        return nums[0]
    }

    left, right := 0, n-1
    for left < right {
        mid := left + (right-left)>>1
        if nums[mid] > nums[right] {
            left = mid + 1
        } else {
            right = mid
        }
    }
    return nums[left]
}
  • 复杂度分析

 

 

 

时间复杂度:O(logn)

空间复杂度:O(1)

1.3.9 Lc154:寻找旋转排序数组最小值II

题目链接

  • 题目描述

 假设按照升序排序的数组在预先未知的某个点上进行了旋转。(例如,数组[0,1,2,4,5,6,7]可能变为[4,5,6,7,0,1,2])。

请找出其中最小的元素。

注意数组中可能存在重复的元素

说明:

这道题是“寻找旋转排序数组中的最小值”的延伸题目。

允许重复会影响算法的时间复杂度吗?会如何影响,为什么?

  • ​​​​​​​​​​​​​​示例
示例 1:
    输入:nums = [1,3,5]
    输出:1

示例 2:
    输入:nums = [2,2,2,0,1]
    输出:0
  • 代码实现

说明:153与154区别在于154允许使用重复元素,这会使情况变得复杂些:

此时153的解法便不再适用。需要特别处理nums[mid]==nums[right]的情况,因为最小值在right右边,因此right一步一步向左逼近。

func findMin(nums []int) int {
    n := len(nums)
    if n == 1 {
        return nums[0]
    }
    left, right := 0, n-1
    for left < right {
        mid := left + (right-left)>>1
        if nums[mid] > nums[right] {
            left = mid + 1
        } else if nums[mid] < nums[right] {
            right = mid
        } else {
            right--
        }
    }
    return nums[left]
}

 

 

 

 

  • 复杂度分析

时间复杂度:O(logn)

空间复杂度:O(1)

1.3.10  模板三

关于模板三,平常使用的比较少,基本使用模板一二就可以搞定,Leetcode官网上给出了这种方案,但是没有给出此模板实现代码。我是用的Java模板转换过来的。由于官方题解中目前还没有看到使用此模板的解法,例子中只是自己的实现,没有完全参考模板,请选择性阅读。

  • ​​​​​​​​​​​​​​​​​​​​​ 模板代码
int binarySearch(nums []int, target int) {
    if (nums == nil || len(nums) == 0)
        return -1;

    left, right := 0, len(nums)- 1;
    for left + 1 < right{
        // Prevent (left + right) overflow
        int mid = left + (right - left) >> 1;
        if nums[mid] == target {
            return mid;
        } else if nums[mid] < target {
            left = mid;
        } else {
            right = mid;
        }
    }
    // Post-processing:
    // End Condition: left + 1 == right
    if nums[left] == target return left;
    if nums[right] == target return right;
    return -1;
}
  • 实现二分查找的另一种方法。
  • 搜索条件需要访问元素的直接左右邻居。
  • 使用元素的邻居来确定它是向右还是向左。
  • 保证查找空间在每个步骤中至少有 3 个元素。
  • 需要进行后处理。当剩下2个元素时,循环/递归结束。需要评估其余元素是否符合条件。
  • 初始条件: ```left=0, right=length - 1```
  • 终止    : ```left == right - 1```
  • 向左查找: ```right = mid```
  • 向右查找: ```left = mid````

​​​​​​​​​​​​​​​​​​​​​ 1.3.11 LC-34:在排序数组中查找元素的第一个和最后一个

原题链接

  • 题目描述

    给定一个按照升序排列的整数数组 nums,和一个目标值target。找出给定目标值在数组中的开始位置和结束位置。如果数组中不存在目标值target,返回[-1, -1]。

进阶:

    你可以设计并实现时间复杂度为O(log n) 的算法解决此问题吗?

  • 示例
示例 1:
输入:nums = [5,7,7,8,8,10], target = 8
输出:[3,4]

示例 2:
输入:nums = [5,7,7,8,8,10], target = 6
输出:[-1,-1]

示例 3:
输入:nums = [], target = 0
输出:[-1,-1]
  • 实现
func searchRange(nums []int, target int) []int {
    if nums == nil {
        return nil
    }
    left, right := 0, len(nums)-1
    for left <= right {
        mid := left + (right-left)>>1

        if nums[mid] == target {
            right = mid
            left = mid
            for left > 0 && nums[left-1] == nums[left] {
                left--
            }
            for right < len(nums)-1 && nums[right+1] == nums[right] {
                right++
            }
            return []int{left, right}
        } else if nums[mid] < target {
            left = mid + 1
        } else if nums[mid] > target {
            right = mid - 1
        }
    }
    return []int{-1, -1}
}

说明:

    实现过程中,在找到目标target情况下,不再使用二分法,而是直接向左向右遍历找到第一个和最后一个出现的位置。之前看到过完全使用二分法的实现,在,完全可以参考模板二中的LC278代码实现两个函数,分别用来查询第一个和最后一个target位置。

  • 复杂度分析

时间复杂度:log(n)

空间复杂度:log(1)

​​​​​​​1.3.12​​​​​​​​​​​​​​ LC-658:找到K个最接近的元素

原题链接

  • 题目描述

 给定一个排序好的数组arr,两个整数k和x,从数组中找到最靠近x(两数之差最小)的k个数。返回的结果必须要是按升序排好的。

整数a比整数b更接近x需要满足

|a - x| < |b - x|  或者

|a - x| == |b - x| 且 a < b

  • 示例
示例 1:
输入:arr = [1,2,3,4,5], k = 4, x = 3
输出:[1,2,3,4]

示例 2:
输入:arr = [1,2,3,4,5], k = 4, x = -1
输出:[1,2,3,4]
  • 代码实现

刷题的过程中写了两种方法,除此之外,在网上题解中看到另外一种二分法解题思路,很简洁,因此也会将其收录下来学习下。

  • 二分法+双指针
  • 双指针
  • 二分法

下面分别给出三种实现方式:

二分法+双指针

func findClosestElements2(arr []int, k int, x int) []int {
    n := len(arr)
    if n < k {
        return nil
    }
    if arr[0] > x {
        return arr[:k] /*左闭右开*/
    }
    if arr[n-1] < x {
        return arr[n-k:]
    }

    left, right := 0, n-1
    for left+1 < right {
        mid := left + (right-left)>>1
        if arr[mid] > x {
            right = mid
        } else if arr[mid] < x {
            left = mid
        } else {
            break
        }
    }
    lk, rk := 0, 0
    if left+1 == right { /*x不存在, left<x<right*/
        lk, rk = left-k+1, right+k-1
    } else { //x存在
        mid := left + (right-left)>>1
        lk, rk = mid-k+1, mid+k-1
    }

    if lk < 0 {
        lk = 0
    }
    if rk > n-1 {
        rk = n - 1
    }
    for lk+k-1 < rk {
        if x-arr[lk] <= arr[rk]-x {
            rk--
        } else {
            lk++
        }
    }
    return arr[lk : rk+1]
}

自己的二分法+双指针代码实现比较啰嗦,除此之外也尝试了下将这者完全融合成一段代码实现,但是非常遗憾,水平有限并没有成功。

双指针

双指针解法也是比较容易理解的。从数组两端分别开始遍历,将里目标值x比较远的元素去除,循环迭代直到长度满足要求为止。代码实现也比较简单,时间复杂度为O(n),空间复杂度为O(1)。当元素数量特别多时,不如二分法效率高。

func findClosestElements(arr []int, k int, x int) []int {
    n := len(arr)
    if n < k {
        return nil
    }
    if arr[0] > x {
        return arr[:k] /*左闭右开*/
    }
    if arr[n-1] < x {
        return arr[n-k:]
    }
    //{1, 2, 3, 4, 5}, 4, 3)
    left, right := 0, n-1
    for left+k-1 < right {
        if x-arr[left] <= arr[right]-x {
            right--
        } else {
            left++
        }
    }

    //End Condition: left+k-1 == right
    return arr[left : right+1]
}

二分法

这个二分法实现虽然仍然不同于模板三,但是比较接近,同时很简洁,非常值得学习一下。(解题思路:二分查找左边界的开始,注意不是查找区间,而是查找正确区间的左值)

func findClosestElements(arr []int, k int, x int) []int {
    start := 0
    end := len(arr) - k

    for start < end {
        mid := start + (end-start)/2

        if x-arr[mid] > arr[mid+k]-x {
            start = mid + 1
        } else {
            end = mid
        }
    }
    return arr[start : start+k]
}

时间复杂度:log(n)

空间复杂度:log(1)​​​​​​​​​​​​​​​​​​​​​​​​​​​​

​​​​​​​1.4​​​​​​​ 小结

    很多二分查找问题都归结于这三种模板中的一个,有一些问题也可以使用多个模板进行解决(比如模板三种的例子,我都是其他其他模板解决的)。三个模板的主要差别在于:

  • 左右中索引的分配
  • 循环或者递归终止条件不同
  • 后续处理的必要性(做题时很多根本不需要后续处理)

模板 #1 (left <= right)

  • 二分查找的最基础和最基本的形式。
  • 查找条件可以在不与元素的两侧进行比较的情况下确定(或使用它周围的特定元素)。
  • 不需要后处理,因为每一步中,你都在检查是否找到了元素。如果到达末尾,则知道未找到该元素。

模板 #2 (left < right)

  • 一种实现二分查找的高级方法。
  • 查找条件需要访问元素的直接右邻居。
  • 使用元素的右邻居来确定是否满足条件,并决定是向左还是向右。
  • 保证查找空间在每一步中至少有 2 个元素。
  • 需要进行后处理。 当你剩下 1 个元素时,循环 / 递归结束。 需要评估剩余元素是否符合条件。

模板 #3 (left + 1 < right)

  • 实现二分查找的另一种方法。
  • 搜索条件需要访问元素的直接左右邻居。
  • 使用元素的邻居来确定它是向右还是向左。
  • 保证查找空间在每个步骤中至少有 3 个元素。
  • 需要进行后处理。 当剩下 2 个元素时,循环 / 递归结束。 需要评估其余元素是否符合条件。

模板一

在有序序列中查找某一个数

模板二

在有序序列中寻找第一次出现的数(修改后也可以最后一次)

模板三

在有序序列中查询最值

 

我整理的是Word版本的,但是Word到MD格式全乱了,如果有需要word版本的,欢迎留言。Word版本内容持续更新中

 
 
 
 
 
 
 
 
 
 
 
 
posted @ 2021-03-13 23:18  叨陪鲤  阅读(73)  评论(0编辑  收藏  举报