双端队列 deque
已上传的图片

题目考点

这道题主要考的是这几个知识点:

  1. 双端队列 deque
  2. 字符串输入解析
  3. 条件判断 if / elif
  4. 循环处理多条命令
  5. 按题目要求输出结果

这道题本质上不是在考你写复杂算法,而是在考你:

能不能把一行一行命令读进来,再正确地作用到数据结构上。

所以,这题的核心不是“算”,而是“根据命令操作容器”。


审题

输入是什么

第一行是一个整数 N,表示一共有多少次操作。

接下来 N 行,每一行是一条命令,可能是下面四种之一:

  • append x
  • appendleft x
  • pop
  • popleft

输出是什么

最后输出双端队列 d 中剩余的所有元素,并且元素之间要用空格隔开。

题目要求我们做什么

题目要求你先准备一个空的双端队列 d,然后按顺序执行每一条命令,最后把里面的内容打印出来。

容易忽略的点

这里有一个非常重要的细节:

有些命令有参数,有些没有参数。

比如:

  • append 1 有参数 1
  • appendleft 4 有参数 4
  • pop 没有参数
  • popleft 没有参数

所以你不能把所有命令都按“方法名 + 参数”来处理,否则遇到 poppopleft 就会出问题。


思路提示

先不要急着写代码,先把这题脑子里变成一个流程。

你可以这样想:

第一步:准备一个空的双端队列

题目已经告诉你要对空的 deque 操作,所以先创建一个空的 d

第二步:循环读入每一条命令

因为第一行给了 N,说明后面正好有 N 条操作,所以我们循环 N 次,每次读一行。

第三步:把每一行命令拆开

比如输入这一行:

appendleft 4

你把它 split() 之后,就会变成:

['appendleft', '4']

再比如:

pop

拆开后就是:

['pop']

所以,我们通常先取第 0 个位置,判断这条命令是什么。

第四步:根据命令执行对应方法

  • 如果是 append,就把后面的值加到右边
  • 如果是 appendleft,就把后面的值加到左边
  • 如果是 pop,就从右边删
  • 如果是 popleft,就从左边删

第五步:输出 deque 里的内容

题目要求空格分隔,所以最后把 d 里的元素按空格输出即可。


完整设计思路

现在把这题完整拆成几个明确步骤。

第一步:导入 deque

Python 里双端队列不是直接内置名字,要从 collections 里导入:

from collections import deque

然后创建空双端队列:

d = deque()

第二步:读入操作次数

第一行是 N,表示总共有多少条命令:

n = int(input())

第三步:循环读命令

每次读一行,并用 split() 分开:

command = input().split()

这样:

  • append 1 会变成 ['append', '1']
  • pop 会变成 ['pop']

所以,command[0] 一定是方法名。

第四步:分类处理命令

这是最关键的部分。

情况 1:append

如果 command[0] == 'append',说明要往右边加元素:

d.append(command[1])

情况 2:appendleft

如果 command[0] == 'appendleft',说明要往左边加元素:

d.appendleft(command[1])

情况 3:pop

如果 command[0] == 'pop',说明删右边最后一个元素:

d.pop()

情况 4:popleft

如果 command[0] == 'popleft',说明删左边第一个元素:

d.popleft()

第五步:输出结果

最后要求按空格打印,所以最方便的写法是:

print(*d)

这个 *d 的意思可以先简单理解成:

把 deque 里的所有元素一个一个拿出来交给 print()

所以它会自动用空格隔开输出。


代码实现

下面给你一份最基础、最适合初学者的写法。

from collections import deque

d = deque()

n = int(input())

for _ in range(n):
    command = input().split()

    if command[0] == 'append':
        d.append(command[1])
    elif command[0] == 'appendleft':
        d.appendleft(command[1])
    elif command[0] == 'pop':
        d.pop()
    elif command[0] == 'popleft':
        d.popleft()

print(*d)

运行演示

我们用题目的样例手动走一遍。

输入:

6
append 1
append 2
append 3
appendleft 4
pop
popleft

初始状态

d = deque([])

第 1 条命令:append 1

右边加入 1

d = deque(['1'])

第 2 条命令:append 2

右边加入 2

d = deque(['1', '2'])

第 3 条命令:append 3

右边加入 3

d = deque(['1', '2', '3'])

第 4 条命令:appendleft 4

左边加入 4

d = deque(['4', '1', '2', '3'])

第 5 条命令:pop

删除右边最后一个元素 3

d = deque(['4', '1', '2'])

第 6 条命令:popleft

删除左边第一个元素 4

d = deque(['1', '2'])

最后输出

1 2

这就和题目样例一致了。


这题最核心的理解

这道题真正的关键可以压缩成一句话:

每读到一条字符串命令,就把它翻译成对 deque 的一次真实操作。

也就是说,这题不是在考你“会不会 deque”,而是在考你:

能不能把输入的命令文本,正确映射成代码行为。

例如:

输入命令对应代码行为
append 5d.append('5')
appendleft 8d.appendleft('8')
popd.pop()
popleftd.popleft()

所以以后你看到这种题,要立刻想到:

  1. 先读操作次数
  2. 每次读一条命令
  3. split() 拆开
  4. if / elif 判断命令类型
  5. 执行对应方法

补充说明:为什么这里常写 command = input().split()

这是这类题的一个非常常见模板。

因为像下面这些输入:

append 1
remove 5
insert 0 10

本质上都是:

一行里有“命令名 + 参数们”

split() 最适合做的事,就是把一行按空格切开,变成列表。

比如:

command = input().split()

如果输入是:

append 1

那么:

command == ['append', '1']

于是:

  • command[0] 是命令名
  • command[1] 是参数

这就是为什么这类“命令题”里,input().split() 往往是核心模板。


补充说明:为什么这里不一定要把数字转成 int

你可能会问:

这里的 123 看起来像数字,为什么代码里直接用 command[1],不写 int(command[1])

原因是:

这道题最后只是要求你把元素打印出来,并没有要求拿它们做加减乘除。

所以把它们当字符串存进去也完全可以,最后照样输出正确结果。

当然,如果你愿意,也可以写成:

d.append(int(command[1]))

也是能过的。

这里只是为了让代码更简单,直接存字符串就够了。


方法总结

以后做这种题,你可以按这个模板审题:

第一步:看题目是不是“命令驱动型”

如果题目给你很多行操作,例如:

  • append
  • insert
  • remove
  • pop
  • reverse

这类题往往不是数学题,而是“按命令操作数据结构”的题。

第二步:先确定要操作的数据结构

这题是 deque,有些题可能是:

  • list
  • set
  • dict

先搞清楚你要操作谁。

第三步:每条命令先 split()

把一整行拆成列表,再取:

  • command[0] 看命令类型
  • command[1]command[2] 看参数

第四步:用 if / elif 一条条映射

把“文本命令”映射成“真实代码”。

这一步是这类题的通用套路。


本节小结

这道题最应该学会的,不只是 deque 的四个方法,而是这整类题的解题方式:

  • 先读操作次数
  • 再逐条读取命令
  • split() 拆分命令
  • 用条件判断执行对应操作
  • 最后按要求输出结果

也就是说,这类题的思维模式是:

题目给我“操作说明”,我就按顺序去驱动一个数据结构。


练习

你可以马上做一道同类型的小练习。

练习题

有一个空列表 lst,输入若干条命令,命令可能是:

  • append x
  • remove x
  • pop

请在所有命令执行结束后,输出列表中的元素,元素之间用空格隔开。

提示

你可以模仿这道题的思路:

  1. 先创建空列表
  2. 循环读取每条命令
  3. split() 拆开
  4. 判断命令类型并执行对应操作
  5. 最后输出列表内容

print(*d) 到底是什么意思

这句代码里,最核心的不是 print,而是前面的这个星号:

print(*d)

这里的 *d 不是乘法。

它在这里表示一种很重要的用法,叫做:

解包,或者你也可以先理解成:把容器里的元素一个一个拆出来。


先看 print(d)print(*d) 的区别

假设有这样一个双端队列:

from collections import deque

d = deque(['1', '2', '3'])

写法 1:print(d)

print(d)

输出是:

deque(['1', '2', '3'])

这是因为你把整个 d 当成一个整体打印出来了。


写法 2:print(*d)

print(*d)

输出是:

1 2 3

这是因为 *dd 里面的元素拆开了,相当于变成:

print('1', '2', '3')

print() 默认会在多个参数之间加空格,所以结果就是:

1 2 3

*d 可以怎么理解

你可以先把它理解成一句非常朴素的话:

d 这个容器打开,把里面的元素逐个拿出来。

比如:

d = deque(['1', '2', '3'])
print(*d)

可以脑子里想象成:

print('1', '2', '3')

这就是为什么这道题最后能直接这样输出。


为什么 print(*d) 正好适合这道题

因为题目要求的是:

按空格分隔输出双端队列里的元素。

print() 本身就有一个特点:

如果你给它多个参数,它会默认用空格连接。

比如:

print('a', 'b', 'c')

输出:

a b c

所以:

print(*d)

就是:

  1. 先把 d 拆成多个元素
  2. 再交给 print()
  3. print() 自动用空格隔开输出

这就刚好符合题目要求。


更具体一点:print() 本来就能接收很多参数

比如:

print(10, 20, 30)

输出:

10 20 30

说明 print() 不是只能打印一个东西,它可以打印很多个参数。

*d 的作用,就是把一个容器,变成“很多个参数”。

所以:

print(*d)

本质上就是把容器中的每个元素,当成 print() 的独立参数传进去。


一个非常直观的类比

假设有一个盒子:

d = ['苹果', '香蕉', '橘子']

不解包

print(d)

相当于你把整个盒子端给别人看。

别人看到的是:

['苹果', '香蕉', '橘子']

解包

print(*d)

相当于你把盒子打开,把里面的东西一个一个摆出来。

别人看到的是:

苹果 香蕉 橘子

* 只在这里有这个意思吗

不是。

* 在 Python 里有不同场景,不同含义。

场景 1:乘法

a = 3 * 5

这里的 * 是乘法。


场景 2:函数调用时解包

print(*d)

这里的 * 是解包。


场景 3:函数定义时收集参数

def func(*args):
    print(args)

这里的 *args 是“把多个参数收集起来”。


你现在这题里遇到的,是第 2 种:

函数调用时的解包。


*d 适用于哪些数据类型

不只是 deque,只要是可以遍历的容器,通常都能这样用。

比如:

列表

a = [1, 2, 3]
print(*a)

输出:

1 2 3

元组

a = (1, 2, 3)
print(*a)

输出:

1 2 3

字符串

s = "abc"
print(*s)

输出:

a b c

因为字符串也可以一个字符一个字符地拆开。

deque

from collections import deque
d = deque([1, 2, 3])
print(*d)

输出:

1 2 3

为什么 deque 也能被 * 拆开

因为 deque 是一种可迭代对象

你现在可以先把“可迭代”理解成:

可以一个一个取出里面元素的东西。

比如我们可以这样写:

for x in d:
    print(x)

说明 deque 里的元素本来就能被一个一个访问。

既然它能一个一个拿出来,那么 *d 就能把它们拆开。


手动模拟一下 print(*d)

假设:

d = deque(['1', '2'])

执行:

print(*d)

你可以脑中把它理解成这两步:

第一步:解包

*dd 拆成:

'1', '2'

第二步:传给 print

变成:

print('1', '2')

第三步:print() 默认用空格连接

输出:

1 2

如果写成 print(d) 为什么不行

因为题目要的是:

1 2

而不是:

deque(['1', '2'])

print(d) 打印的是这个对象本身的样子。

print(*d) 打印的是对象里面的元素。

这是本质区别。


如果 d 是空的,会怎么样

如果:

d = deque()
print(*d)

那么不会报错,只是输出一行空行。

因为“拆开后没有元素”,那 print() 就相当于没收到内容。


再补一层:print(*d, sep='-') 又是什么意思

因为 print() 默认参数之间用空格隔开,但你也可以改。

例如:

d = deque(['1', '2', '3'])
print(*d, sep='-')

输出:

1-2-3

说明:

  • *d 负责拆开
  • sep='-' 负责指定分隔符

所以这题之所以不写 sep,是因为题目本来就要求空格分隔,而 print() 默认就是空格。


一句话总结 *d

你可以直接记住这句话:

*d 的意思就是:把容器 d 里面的元素一个一个拆出来,作为多个独立参数传给函数。

所以:

print(*d)

就等价于:

print(d里的第1个元素, d里的第2个元素, d里的第3个元素, ...)

本节小结

这句代码里:

print(*d)
  • d 是一个双端队列
  • *d 不是乘法,而是解包
  • 解包就是把 d 里的元素一个个拆出来
  • print() 接收到多个参数后,会默认用空格输出
  • 所以它特别适合这道题的“空格分隔输出”

小练习

你先自己判断下面三句各会输出什么:

a = [10, 20, 30]

print(a)
print(*a)
print(*a, sep='-')

提示

你重点想这三个问题:

  1. 是打印整个列表,还是打印列表里的元素?
  2. 有没有解包?
  3. 分隔符是默认空格,还是 -

先给结论:这两个 * 长得像,但方向正好相反

你现在看到的这两种写法:

print(*d)

def func(*args):
    ...

它们虽然都写了一个 *,但做的事情正好是反着来的。

你可以直接先记这一句:

  • 函数调用里的 *:拆开
  • 函数定义里的 *:收集

也就是:

  • 调用时:一个容器 -> 多个参数
  • 定义时:多个参数 -> 一个容器

这就是你说的“看起来像,但本质上方向正好相反”。


一、先看函数调用里的 *:它是在“拆开”

例如:

nums = [1, 2, 3]
print(*nums)

这里的 *nums,意思不是把 nums 当成一个整体传进去,而是把它拆开,相当于:

print(1, 2, 3)

所以这里的 * 是:

展开、解包、拆开。

这一步的数据流向

原来是一个列表:

[1, 2, 3]

经过 * 之后,变成多个独立参数:

1, 2, 3

所以这是:

一变多。


二、再看函数定义里的 *:它是在“收集”

例如:

def func(*args):
    print(args)

然后你调用:

func(1, 2, 3)

输出是:

(1, 2, 3)

这里发生了什么?

调用时你给了 3 个独立参数:

1, 2, 3

但函数内部的 args 把它们收起来,变成了一个元组:

(1, 2, 3)

所以这里的 * 是:

收集、打包。

这一步的数据流向

原来是多个参数:

1, 2, 3

进入函数后,变成一个整体:

(1, 2, 3)

所以这是:

多变一。


三、为什么说它们“方向相反”

现在把两边并排看,你就会非常清楚。

1. 函数调用时的 *

nums = [1, 2, 3]
print(*nums)

含义:

把一个列表拆成多个参数

也就是:

[1, 2, 3]  ->  1, 2, 3

2. 函数定义时的 *

def func(*args):
    print(args)

调用:

func(1, 2, 3)

含义:

把多个参数收成一个元组

也就是:

1, 2, 3  ->  (1, 2, 3)

所以你会发现:

  • 调用里的 *:是把“整体”拆散
  • 定义里的 *:是把“分散的”收拢

这就是“方向相反”的真正意思。


四、你可以把它理解成“门口”和“屋里”的两个动作

这是一个很适合初学者记忆的比喻。

假设函数是一间房子。

在房子外面调用函数时

你手里拿着一个箱子:

[1, 2, 3]

如果你写:

print(*nums)

就像是你在进门前,把箱子打开,把里面三个东西一个个拿出来交给函数。

所以是:

进门前拆开。


在房子里面定义函数时

你写:

def func(*args):

就像是函数说:

“你不管给我多少个东西,我都帮你装进一个袋子里,袋子名字叫 args。”

所以是:

进门后收集。


五、把这两步连起来看,会更透

下面这个例子特别值得看透。

def func(*args):
    print(args)

nums = [1, 2, 3]
func(*nums)

我们一点一点看。


第一步:调用时 *nums

func(*nums)

这里是函数调用,所以 *nums 先把列表拆开:

func(1, 2, 3)

这是“解包”。


第二步:函数定义里的 *args

函数头是:

def func(*args):

意思是把传进来的多个参数收集到 args 里。

所以:

func(1, 2, 3)

进入函数后,相当于:

args = (1, 2, 3)

这是“打包”。


最终结果

所以这一整句:

func(*nums)

你可以理解成:

  1. 先把 nums 拆开
  2. 再被 *args 收进去

也就是:

[1, 2, 3] -> 1, 2, 3 -> (1, 2, 3)

前半段是“拆”,后半段是“收”。

这就是为什么它们看起来都是 *,但其实是在不同位置扮演反方向的角色。


六、为什么 Python 要这么设计

因为函数调用和函数定义,本来就是两个相反的过程。

函数调用

是你把数据“送进去”。

所以如果你手上拿着一个列表,Python 允许你用 * 把它拆成多个参数,更方便传参。

例如:

nums = [3, 5]
print(max(*nums))

相当于:

print(max(3, 5))

函数定义

是函数在“接收数据”。

所以 Python 允许你写 *args,表示:

“不管你给我几个位置参数,我都能统一接住。”

例如:

def total(*args):
    return sum(args)

这样你可以写:

total(1, 2, 3)
total(10, 20)
total(7)

函数都能接住。


七、最容易混淆的地方

很多初学者看到这两个 *,会误以为:

“既然都是星号,那意思应该差不多吧?”

其实不对。

它们只有外形一样,但语境不同。

在函数调用里

func(*x)

这里你已经有一个容器 x 了,你要做的是:

把容器展开给函数看。


在函数定义里

def func(*x):

这里函数还没接到参数,它要做的是:

把传进来的多个参数收集到变量 x 中。


所以它们的共同点只是都和“参数处理”有关,但动作正好相反:

  • 一个是把整体拆成多个
  • 一个是把多个合成整体

八、用箭头记忆,会非常牢

你可以直接记下面这两条。

函数调用

func(*data)

记成:

一个容器  ->  多个参数

也就是:

[1, 2, 3] -> 1, 2, 3

函数定义

def func(*args):

记成:

多个参数  ->  一个元组

也就是:

1, 2, 3 -> (1, 2, 3)

九、顺手补一下:** 也是同样逻辑

虽然你现在重点是 *,但顺手知道一下 ** 会更完整。

调用时 **

d = {'sep': '-', 'end': '!\n'}
print(1, 2, 3, **d)

这里是把字典拆开成关键字参数。


定义时 **kwargs

def func(**kwargs):
    print(kwargs)

这里是把多个关键字参数收集成字典。

所以和 * 一样,** 也是:

  • 调用时:拆开
  • 定义时:收集

只是 * 处理的是位置参数,** 处理的是关键字参数。


十、最后把你这次的问题压成一句真正能记住的话

你可以这样理解:

函数调用时的 *,站在“传参的人”角度,是把一个容器拆开。
函数定义时的 *,站在“收参的函数”角度,是把多个参数收起来。

也就是说:

  • 调用者在“展开”
  • 被调用者在“接住”

一个往外摊开,一个往里归拢,所以方向正好相反。


本节小结

最核心的结论就是这四句话:

1

print(*d)

表示把 d 里的元素拆开,传给 print()

2

def func(*args):

表示把传进来的多个位置参数,收集到 args 这个元组里。

3

函数调用里的 * 是:

解包、展开、拆开。

4

函数定义里的 * 是:

收集、打包、归拢。


小练习

你自己先手动分析下面这段代码,不要急着运行:

def show(*args):
    print(args)

a = [10, 20, 30]
show(*a)

提示

你按这两步想:

第一步,show(*a) 先会变成什么。
第二步,进入 def show(*args) 后,args 又会变成什么。

文末附加内容
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇