今天给大家带来一篇面试高频算法题之栈队列的详细解析,全文包含9道大厂笔试面试算法真题,一举拿下栈和队列这个知识点,让算法不在成为进入大厂的绊脚石。1栈和队列 全文概览 基础知识栈 栈是一种先进后出的数据结构。这里有一个非常典型的例子,就是堆叠盘子。我们在放盘子的时候,只能从下往上一个一个的放;在取的时候,只能从上往下一个一个取,不能从中间随意取出。 栈是一种操作受限的线性表,只允许在一端处理数据。主要包括两种操作,即入栈和出栈,也就是在栈顶插入一个数据和从栈顶删除一个数据。 栈既可以用数组实现,也可以用链表来实现。用数组实现的栈,我们叫作顺序栈,用链表实现的栈,我们叫作链式栈。队列 队列是一种先进先出的数据结构。你可以把它想象成排队买票,先来的先买,后来的人只能站末尾,不允许插队。 队列跟栈一样,也是一种操作受限的线性表数据结构。主要包括两个操作,即出队和入队,也就是从队首取一个元素和在队尾插入一个元素。 队列可以用数组来实现,也可以用链表来实现。用数组实现的队列叫作顺序队列,用链表实现的队列叫作链式队列。2用两个栈实现队列 剑指Offer09。用两个栈实现队列问题描述 用两个栈来实现一个队列,完成n次在队列尾部插入整数(push)和在队列头部删除整数(pop)的功能。队列中的元素为int类型。保证操作合法,即保证pop操作时队列内已有元素。 示例: 输入:〔PSH1,PSH2,POP,POP〕 返回值:1,2 说明: PSH1:代表将1插入队列尾部PSH2:代表将2插入队列尾部POP:代表删除一个元素,先进先出返回1POP:代表删除一个元素,先进先出返回2分析问题 首先,我们需要知道队列和栈的区别。队列是一种先进先出的数据结构。队列中的元素是从后端入队,从前端出队。就和排队买票一样。栈是一种后进先出的数据结构。栈中的元素是从栈顶压入,从栈顶弹出。 为了使用两个栈实现队列的先进先出的特性,我们需要用一个栈来反转元素的入队顺序。 入队操作Push: 因为栈是后进先出的,而队列是先进先出的,所以要想使用栈来实现队列的先进先出功能,我们需要把新入栈的元素放入栈底。为了实现这个操作,我们需要先把栈S1中的元素移动到S2,接着再把新来的元素压入S2,然后再把S2中的所有元素再弹出,压入到S1。这样就实现了把新入栈的元素放入栈底的功能。 defpush(self,node):writecodeherewhileself。stack1:self。stack2。append(self。stack1。pop())self。stack2。append(node)whileself。stack2:self。stack1。append(self。stack2。pop()) 出队操作Pop: 我们直接从S1弹出就可以了,因为经过反转后,S1中的栈顶元素就是最先入栈的元素,也就是队首元素。defpop(self):ifself。stack1:returnself。stack1。pop() 我们来看完整的代码实现。classSolution:definit(self):self。stack1〔〕self。stack2〔〕defpush(self,node):writecodeherewhileself。stack1:self。stack2。append(self。stack1。pop())self。stack2。append(node)whileself。stack2:self。stack1。append(self。stack2。pop())defpop(self):ifself。stack1:returnself。stack1。pop() 我们可以看到入队操作的时间复杂度是O(n),空间复杂度也是O(n)。出队时间复杂度是O(1),空间复杂度也是O(1)。优化 在上面的算法中,不知道你有没有发现,每次在push一个新元素时,我们都需要把S1中的元素移动到S2中,然后再从S2移回到S1中。这显然是冗余的。其实,我们在入队时只需要插入到S1中即可。而出队的时候,由于第一个元素被压在了栈S1的底部,要想实现队列的先进先出功能,我们就需要把S1的元素进行反转。我们可以把栈S1的元素Pop出去,然后压入S2。这样就把S1的栈底元素放在了栈S2的栈顶,我们直接从S2将它弹出即可。一旦S2变空了,我们只需把S1中的元素再一次转移到S2就可以了。 下面我们来看一下代码实现。classSolution:definit(self):self。stack1〔〕self。stack2〔〕defpush(self,node):writecodehereself。stack1。append(node)defpop(self):ifnotself。stack2:whileself。stack1:self。stack2。append(self。stack1。pop())returnself。stack2。pop()3有效的括号 LeetCode20。有效的括号问题描述 给定一个只包括(,),{,},〔,〕的字符串s,判断字符串是否有效。 有效字符满足的条件是:左括号必须用相同类型的右括号闭合。左括号必须以正确的顺序闭合。 示例: 输入:s()〔〕{} 输出:true分析问题 这个问题我们可以借助栈这种数据结构来解决。在遍历字符串的过程中,当我们遇见一个左括号时,我们就入栈,当我们遇到一个右括号时,我们就取出栈顶元素去判断他们是否是同类型的。如果不是的话,那就代表字符串s不是有效串,我们直接返回False。如果是,接着去遍历,直到遍历结束为止。当遍历完字符串s后,如果栈为空,就代表字符串是有效的。这里需要注意一点,为了加快判断左、右括号是否是同类型的,我们引入哈希表存储每一种括号。哈希表的键为右括号,值为相同类型的左括号。 下面我们来看一下代码实现。defisValid(s):如果字符串不是偶数,直接返回false因为字符只包含括号,所以只有偶数时才有可能匹配上iflen(s)21:returnFalsedict{):(,〕:〔,}:{,}stacklist()forchins:代表遍历到右括号ifchindict:看栈顶元素是否能匹配上,如果没有匹配上,返回falseifnotstackorstack〔1〕!dict〔ch〕:returnFalse如果匹配上,弹出栈顶元素stack。pop()else:匹配到左括号,入栈stack。append(ch)栈为空,代表s是有效串,否则是无效串returnnotstack4包含min函数的栈 剑指Offer30。包含min函数的栈问题描述 定义栈的数据结构,请在该类型中实现一个能够得到栈中所含最小元素的min函数,并且调用min函数、push函数及pop函数的时间复杂度都是O(1)。 push(value):将value压入栈中 pop():弹出栈顶元素 top():获取栈顶元素 min():获取栈中最小元素 示例:minStack。push(2);将2入栈minStack。push(0);将0入栈minStack。push(3);将3入栈minStack。min();返回栈中最小元素3minStack。pop();弹出栈顶元素3minStack。top();返回栈顶元素0minStack。min();返回栈中最小元素2分析问题 对于普通的栈来说,执行push和pop的时间复杂度是O(1),而执行min函数的时间复杂度是O(N),因为要想找到最小值,就需要遍历整个栈,为了降低min函数的时间复杂度,我们引入了一个辅助栈。数据栈A:栈A用来存储所有的元素,保证入栈push、出栈pop、获取栈顶元素top的操作。辅助栈B:栈B用来存储栈A中所有非严格递减的元素,即栈A中的最小值始终在栈B的栈顶,这样可以保证以O(1)的时间复杂度来返回栈中的最小值。 下面我们来看一下代码实现。classSolution:definit(self):数据栈self。A〔〕辅助栈self。B〔〕push操作defpush(self,x):self。A。append(x)如果辅助栈B为空,或者栈顶元素大于x,则入栈ifnotself。Borself。B〔1〕x:self。B。append(x)defpop(self):弹出数据栈A中的元素sself。A。pop()如果弹出的元素和栈B的栈顶元素相同,则为了保持一致性将栈B的栈顶元素弹出ifsself。B〔1〕:self。B。pop()deftop(self):返回数据栈A中的栈顶元素returnself。A〔1〕defmin(self):返回辅助栈B中的栈顶元素returnself。B〔1〕5表达式求值问题描述 请写一个整数计算器,支持加减乘三种运算和括号。 示例: 输入:(2(34)))5 返回值:10分析问题 因为只支持加、减、乘、括号,所以我们根据优先级可以分为3类,即括号乘加、减,假设先把括号去掉,那么就剩下乘和加减运算,根据运算规则,我们需要先计算乘、再计算加、减,因此我们可以这么来考虑,我们先进行乘法运算,并将这些乘法运算后的整数值返回原表达式的相应位置,则随后整个表达式的值,就等于一系列整数加减后的值。而对于被括号分割的表达式,我们可以递归的去求解,具体算法如下。 遍历字符串s,并用变量preSign记录每个数字之前的运算符,初始化为加号。遇到空格时跳过。遇到数字时,继续遍历求出这个完整的数字的值,保存到num中。遇到左括号时,需要递归的求出这个括号内的表达式的值。遇到运算符或者表达式的末尾时,就根据上一个运算符的类型来决定计算方式。如果是加号,不需要进行计算,直接push到栈里如果是减号,就去当前数的相反数,push到栈里如果是乘号,就需要从栈内pop出一个数和当前数求乘法,再把计算结果push到栈中最后把栈中的结果求和即可。 下面我们来看一下代码实现。classSolution:defcalculate(self,s):nlen(s)存取部分数据和stack〔〕preSignnum0i0whilein:cs〔i〕ifc:ii1continueifc。isdigit():numnum10ord(c)ord(0)如果遇到左括号,递归求出括号内表达式的值ifc(:ji1counts1截取出括号表达式的值whilecounts0:ifs〔j〕(:countscounts1ifs〔j〕):countscounts1jj1剥去一层括号,求括号内表达式的值numself。calculate(s〔i1:j1〕)ij1ifnotc。isdigit()orin1:ifpreSign:stack。append(num)elifpreSign:stack。append(1num)elifpreSign:tmpstack。pop()stack。append(tmpnum)num0preSigncii1returnsum(stack)sSolution()print(s。calculate((34)(5(23))))6滑动窗口的最大值 LeetCode239。滑动窗口最大值问题描述 给你一个整数数组nums,有一个大小为k的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的k个数字。滑动窗口每次只向右移动一位。返回滑动窗口中的最大值。 示例: 输入:〔2,3,4,2,6,2,5,1〕,3 输出:〔4,4,6,6,6,5〕分析问题 这道题的关键点在于求滑动窗口中的最大值。大小为k的滑动窗口,我们可以通过遍历的方式来求出其中的最大值,需要O(k)的时间复杂度。对于大小为n的数组nums,一共有nk1个窗口,因此该算法的时间复杂度是O(nk)。 通过观察,我们可以知道,对于两个相邻的滑动窗口,有k1个元素是共用的,只有一个元素是变化的,因此我们可以利用此性质来优化我们的算法。 对于求最大值问题,我们可以使用优先级队列(大顶推)来求解。首先,我们将数组的前k个元素放入优先级队列中。每当我们向右移动窗口时,我们就可以把一个新的元素放入队列中,此时堆顶元素就是堆中所有元素的最大值,然而这个最大值有可能不属于当前的滑动窗口中,我们需要将该元素进行移除处理(如果最大值不在当前滑动窗口中,它只能在滑动窗口的左边界的左侧,所以滑动窗口向右移动的过程中,该元素再也不会出现在滑动窗口中了,所以我们可以对其进行移除处理)。我们不断地移除堆顶的元素,直到其确实出现在滑动窗口中。此时,堆顶元素就是滑动窗口中的最大值。 为了方便判断堆顶元素与滑动窗口的位置关系,我们可以在优先队列中存储二元组(num,index),表示元素num在数组中的下标为index。 小trick:因为python中只提供了小顶堆,所以我们需要对元素进行取反处理,例如对于列表〔1,3〕,我们对元素进行取反,然后插入小顶堆中,此时堆中是这样的〔1,3〕,我们取出堆顶元素1,然后取反为1,正好可以得到列表中的最大值1。 我们nums〔2,3,4,2,6,2,5,1〕,k3为例,来看一下具体的过程。首先,我们将nums的前3个元素放入优先级队列中,队首元素下标值index20,在窗口中,所以加入结果中,此时res〔4〕。下一个元素2入队,此时队首元素下标index21,在窗口中,所以加入结果中,此时res〔4,4〕。下一个元素6入队,此时队首元素下标index42,在窗口中,所以加入结果中,此时res〔4,4,6〕。下一个元素2入队,此时队首元素下标index43,在窗口中,所以加入结果中,此时res〔4,4,6,6〕。下一个元素5入队,此时队首元素下标index44,在窗口中,所以加入结果中,此时res〔4,4,6,6,6〕。下一个元素1队列,此时队首元素下标index45,不在窗口中,所以我们将其弹出,此时队首元素的下标变为6,在窗口中,所以加入结果中,此时res〔4,4,6,6,6,5〕。进阶 这道题我们也可以使用双端队列来求解。我们在遍历数组的过程中,不断的对元素对应的下标进行出队入队操作,在出入队的过程中,我们需要保证队列中存储的下标对应的元素是从大到小排序的。具体来说,当有一个新的元素对应的下标需要入队时,如果该元素比队尾对应的元素的值大,我们需要弹出队尾,然后循环往复,直到队列为空或者新的元素小于队尾对应的元素。 由于队列中下标对应的元素是严格单调递减的,因此队首下标对应的元素就是滑动窗口中的最大值。但是此时的最大值可能在滑动窗口左边界的左侧,并且随着窗口向右移动,它永远不可能出现在滑动窗口中了。因此我们还需要不断从队首弹出元素,直到队首元素在窗口中为止。 我们还是以nums〔2,3,4,2,6,2,5,1〕,k3为例,来看一下具体的过程。我们首先初始化一个空队列que。此时队列为que空,元素2对应的下标0入队。并且此时未形成窗口,不取值。此时队列que〔0〕,队尾元素为0,它对应数组中的元素是nums〔0〕nums〔1〕的,所以我们把队尾0弹出,此时队列为空,我们将1入队。并且此时未形成窗口,不取值。此时队列que〔1〕,队尾元素为1,它对应的数组中的元素是nums〔1〕nums〔2〕的,所以我们把队尾1弹出,此时队列为空,我们将2入队。并且此时队首元素2在窗口〔0,2〕中,所以取出队首元素。此时队列que〔2〕,队尾元素为2,它对应的数组中的元素是nums〔2〕nums〔3〕的,所以我们将3入队。并且此时队首元素2在窗口〔1,3〕中,所以取出队首元素。此时队列que〔2,3〕,队尾元素为3,它对应的数组中的元素是nums〔3〕nums〔4〕的,所以我们把队尾3弹出,并且此时队尾元素对应的数组中的元素是nums〔2〕nums〔4〕,所以我们把队尾2弹出,此时队列为空,我们将4入队。并且此时队首元素4在窗口〔2,4〕中,所以取出队首元素。此时队列que〔4〕,队尾元素为4,它对应的数组中的元素是nums〔4〕nums〔5〕的,所以我们将5入队。并且此时队首元素4在窗口〔3,5〕中,所以我们取出队首元素。此时队列que〔4,5〕,队尾元素为5,它对应的数组中的元素是nums〔5〕nums〔6〕的,所以我们把队尾5弹出,此时队尾元素对应的数组中的元素时nums〔4〕nums〔6〕,所以我们将6入队。并且此时队首元素4在窗口〔4,6〕中,所以我们取出队首元素。此时队列que〔4,6〕,队尾元素为6,它对应的数组中的元素是nums〔6〕nums〔7〕的,所以我们将7入队。而此时队首元素4不在窗口〔5,7〕中,所以我们将其移除队列,此时队首元素6在窗口〔5,7〕中,所以我们将其取出。 下面我们来看一下代码实现。importcollectionsclassSolution:defmaxSlidingWindow(self,nums,k):nlen(nums)申请一个双端队列qcollections。deque()初始化第一个窗口foriinrange(k):如果队列不为空且比队尾元素大,将队尾出队whileqandnums〔i〕nums〔q〔1〕〕:q。pop()直到队列为空,或者比队尾元素小,入队q。append(i)将队首元素加入结果中ans〔nums〔q〔0〕〕〕窗口逐步向右移动foriinrange(k,n):如果队列不为空且比队尾元素大,将队尾出队whileqandnums〔i〕nums〔q〔1〕〕:q。pop()直到队列为空,或者比队尾元素小,入队q。append(i)如果队首元素不在该窗口内,出队操作whileq〔0〕ik:q。popleft()将队首元素加入结果中ans。append(nums〔q〔0〕〕)returnanssSolution()print(s。maxSlidingWindow(〔2,3,4,2,6,2,5,1〕,3))7栈和排序问题描述 给你一个由1n,n个数字组成的一个排列和一个栈,要求按照排列的顺序入栈。如何在不打乱入栈顺序的情况下,仅利用入栈和出栈两种操作,输出字典序最大的出栈序列。 排列:指1到n每个数字出现且仅出现一次。 示例: 输入:〔2,4,5,3,1〕 输出:〔5,4,3,2,1〕分析问题 由于我们只能使用出栈和入栈两种操作,要想使得出栈序列字典序最大,首先想到的就是令高位尽可能地大,我们出栈的时机就是:当前入栈元素若是大于之后将要入栈的元素,那么就将其出栈。当元素出栈后,还需要判断栈顶元素与之后将要入栈元素之间的大小关系,如果此时栈顶元素大于之后将要入栈的元素,那么就将其出栈,不断判断直到栈为空或条件不满足。 为了快速判断当前入栈元素是否大于之后将要入栈的元素,我们需要创建一个辅助数组temp,其中temp〔i〕表示i之后的最大元素。借助辅助数组,我们可以以O(1)的时间复杂度去判断当前入栈元素是否大于之后将要入栈的元素。 下面我们来看一下代码的实现。importsysclassSolution:defsolve(self,a):nlen(a)res〔〕ifn0:returnresstack〔〕temp〔0〕ntemp〔n1〕sys。maxsize1从右往左遍历数组a,然后取填充temp使得temp〔i〕表示i之后的最大元素foriinrange(n2,1,1):temp〔i〕max(a〔i1〕,temp〔i1〕)遍历数组aforiinrange(0,n):ifa〔i〕temp〔i〕:若当前元素大于之后将要入栈的元素,将其加入结果中res。append(a〔i〕)若栈不为空,且栈顶元素大于temp〔i〕,栈顶出栈,加入结果中whilestackandstack〔1〕temp〔i〕:res。append(stack〔1〕)stack。pop()else:stack。append(a〔i〕)whilestack:res。append(stack〔1〕)stack。pop()returnres 该算法的时间复杂度是O(n),空间复杂度也是O(n)。8单调栈问题描述 给定一个长度为n的可能含有重复值的数组arr,找到每一个i位置左边和右边离i位置最近且值比arr〔i〕小的位置。请设计算法,返回一个二维数组,表示所有位置相应的信息。位置信息包括:两个数字l和r,如果不存在,则值为1,下标从0开始。 示例: 输入:〔3,4,1,5,6,2,7〕 输出:〔〔1,2〕,〔0,2〕,〔1,1〕,〔2,5〕,〔3,5〕,〔2,1〕,〔5,1〕〕分析问题 这道题最简单的解法就是暴力求解,即通过两层for循环来求解。如下所示:classSolution:deffoundMonotoneStack(self,nums):nlen(nums)res〔〕遍历一遍数组foriinrange(0,n):l1r1从左往右寻找l,寻找比nums〔i〕小的最近的nums〔l〕forjinrange(0,i):ifnums〔j〕nums〔i〕:lj从右往左寻找l,寻找比nums〔i〕小的最近的nums〔r〕forjinrange(n1,i,1):ifnums〔j〕nums〔i〕:rjres。append(〔l,r〕)returnres 该算法的时间复杂度是O(n2),空间复杂度是O(1)。 显然暴力求解的时间复杂度太高,那我们该如何优化呢?其实这道题我们可以使用单调栈来求解,首先我们来看一下单调栈的定义。 单调栈是指栈内元素是具有有单调性的栈,和普通栈相比,单调栈在入栈的时候,需要将待入栈的元素和栈顶元素进行对比,看待加入栈的元素入栈后是否会破坏栈的单调性,如果不会,直接入栈,否则一直弹出到满足条件为止。 本题中,我们维护一个存储数组下标的单调栈。然后遍历数组,执行如下操作。 我们以求每一个i左边离i最近且小于arr〔i〕的位置为例,来看一下算法的执行流程。首先我们从左往右遍历数组。假设遍历到的元素是arr〔i〕,栈顶元素top对应的数组中的元素是arr〔top〕,然后我们拿arr〔i〕和arr〔top〕进行对比。如果arr〔top〕arr〔i〕,就说明top不是第i个元素的解,也不会是i以后任何元素的解(因为i比top距离后面的数更近,同时arr〔i〕arr〔top〕),所以我们就把top弹出,直到栈为空或者栈顶元素(数组的下标)对应数组中的元素小于arr〔i〕。如果arr〔top〕arr〔i〕,就说明第i个数的解就是top,因为栈内的元素都是单调递增的,所以top是离i最近的数,即top就是所求值。然后因为i可能是i右侧的候选解,所以把i加入栈中。 同理,我们从右往左遍历数组,就可以得到每一个i右边离i最近且小于arr〔i〕的位置。 下面我们来看一下代码的实现。classSolution:deffoundMonotoneStack(self,nums):nlen(nums)res〔〕l〔0〕nr〔0〕n单调栈stack〔〕从左往右遍历数组foriinrange(0,n):如果栈顶元素top对应的数组中的元素num〔top〕nums〔i〕则出栈,直到栈为空或者栈顶元素对应的数组中的元素比nums〔i〕小whilestackandnums〔stack〔1〕〕nums〔i〕:stack。pop()l〔i〕stack〔1〕ifstackelse1i入栈,因为i有可能成为i右边的答案stack。append(i)stack〔〕从右往左遍历数组foriinrange(n1,1,1):如果栈顶元素top对应的数组中的元素num〔top〕nums〔i〕则出栈,直到栈为空或者栈顶元素对应的数组中的元素比nums〔i〕小whilestackandnums〔stack〔1〕〕nums〔i〕:stack。pop()r〔i〕stack〔1〕ifstackelse1i入栈,因为i有可能成为i左边的答案stack。append(i)foriinrange(0,len(l)):res。append(〔l〔i〕,r〔i〕〕)returnressSolution()print(s。foundMonotoneStack(〔3,4,1,5,6,2,7〕)) 该算法的时间复杂度是O(n),空间复杂度是O(n)。9每日温度 739。每日温度问题描述 请根据每日气温列表temperatures,计算在每一天需要等几天才会有更高的温度。如果气温在这之后都不会升高,请在该位置用0来代替。 示例: 输入:temperatures〔73,74,75,71,69,72,76,73〕 输出:〔1,1,4,2,1,1,0,0〕分析问题 既然是求每一天需要等几天才会有更高的温度,那么最直观的想法就是针对气温列表temperatures中的每个温度值,向后依次进行搜索,找到第一个比当前温度更高的值的位置索引,然后减去当前温度所在的位置索引,就是要求的结果。 下面我们来看一下代码的实现。classSolution(object):defdailyTemperatures(self,temperatures):求出温度列表的长度nlen(temperatures)result〔0〕n遍历每一个温度值foriinrange(n):iftemperatures〔i〕100:想后搜索第一个大于当前温度值的元素forjinrange(i1,n):iftemperatures〔j〕temperatures〔i〕:result〔i〕jibreakreturnresult 该算法的时间复杂度是O(n2),空间复杂度是O(n)。 显然该算法的时间复杂度太高,那我们有什么优化的方法吗。优化 这里可以使用单调栈来优化,即维护一个温度值下标的单调栈,使得从栈顶到栈底的的元素对应的温度值依次递减。具体来说,我们正向遍历温度列表temperatures。对于温度列表中的每个元素temperatures〔i〕。如果栈为空,则直接将i进栈;如果栈不为空,则比较栈顶元素prev对应的温度值temperatures〔prev〕和当前温度temperatures〔i〕。如果temperatures〔prev〕temperatures〔i〕,则将栈顶元素prev移除,此时prev对应的等待天数为iprev。重复上述操作直到栈为空或者栈顶元素对应的温度大于等于当前温度,然后将i进栈。如果temperatures〔prev〕temperatures〔i〕,则直接将元素i入栈。 下面我们来思考一个问题,为什么可以在出栈的时候更新等待天数result〔prev〕呢?因为即将进栈的元素i对应的温度值temperatures〔i〕一定是temperatures〔prev〕右边第一个比它大的元素。 下面我们来看一个具体的例子,假设温度列表temperatures〔73,74,75,71,69,72,76,73〕。 初始时,单调栈stack为空;等待天数result为〔0,0,0,0,0,0,0,0〕。 下面我们来看一下代码的实现。nlen(temperatures)初始化一个空的栈stack〔〕result〔0〕nforiinrange(n):temperaturetemperatures〔i〕如果temperatures〔i〕大于栈顶元素对应的温度值,则栈顶元素出栈。whilestackandtemperaturetemperatures〔stack〔1〕〕:prevstack。pop()result〔prev〕iprevstack。append(i)returnresult 该算法的时间复杂度是O(n),空间复杂度也是O(n)。 原文链接:https:mp。weixin。qq。coms0Uv7UBKk0tzhWHLQCePmIg