常用数据结构之列表-1
在开始本节课的内容之前,我们先给大家一个编程任务,将一颗色子掷 6000 次,统计每种点数出现的次数。这个任务对大家来说应该是非常简单的,我们可以用 1 到 6 均匀分布的随机数来模拟掷色子,然后用 6 个变量分别记录每个点数出现的次数,相信通过前面的学习,大家都能比较顺利的写出下面的代码。
1 | """ |
上面的代码非常有多么“丑陋”相信就不用我多说了。当然,更为可怕的是,如果我们要掷两颗或者掷更多的色子,然后统计每种点数出现的次数,那就需要定义更多的变量,写更多的分支结构,大家想想都会感到恶心。讲到这里,相信大家心中已经有一个疑问了:有没有办法用一个变量来保存多个数据,有没有办法用统一的代码对多个数据进行操作?答案是肯定的,在 Python 语言中我们可以通过容器型变量来保存和操作多个数据,我们首先为大家介绍列表(list
)这种新的数据类型。
创建列表
在 Python 中,列表是由一系元素按特定顺序构成的数据序列,这就意味着如果我们定义一个列表类型的变量,可以用它来保存多个数据。在 Python 中,可以使用[]
字面量语法来定义列表,列表中的多个元素用逗号进行分隔,代码如下所示。
1 | items1 = [35, 12, 99, 68, 55, 35, 87] |
说明:列表中可以有重复元素,例如
items1
中的35
;列表中可以有不同类型的元素,例如items3
中有int
类型、float
类型、str
类型和bool
类型的元素,但是我们通常并不建议将不同类型的元素放在同一个列表中,主要是操作起来极为不便。
我们可以使用type
函数来查看变量的类型,有兴趣的小伙伴可以自行查看上面的变量items1
到底是什么类型。因为列表可以保存多个元素,它是一种容器型的数据类型,所以我们在给列表类型的变量起名字时,变量名通常用复数形式的单词。
除此以外,还可以通过 Python 内置的list
函数将其他序列变成列表。准确的说,list
并不是一个普通的函数,它是创建列表对象的构造器,后面的课程会为大家介绍对象和构造器这些概念。
1 | items4 = list(range(1, 10)) |
说明:
range(1, 10)
会产生1
到9
的整数序列,给到list
构造器中,会创建出由1
到9
的整数构成的列表。字符串是字符构成的序列,上面的list('hello')
用字符串hello
的字符作为列表元素,创建了列表对象。
列表的运算
我们可以使用+
运算符实现两个列表的拼接,拼接运算会将两个列表中的元素连接起来放到一个列表中,代码如下所示。
1 | items5 = [35, 12, 99, 45, 66] |
我们可以使用*
运算符实现列表的重复运算,*
运算符会将列表元素重复指定的次数,我们在上面的代码中增加两行,如下所示。
1 | print(items6 * 3) # [45, 58, 29, 45, 58, 29, 45, 58, 29] |
我们可以使用in
或not in
运算符判断一个元素在不在列表中,我们在上面的代码代码中再增加两行,如下所示。
1 | print(29 in items6) # True |
由于列表中有多个元素,而且元素是按照特定顺序放在列表中的,所以当我们想操作列表中的某个元素时,可以使用[]
运算符,通过在[]
中指定元素的位置来访问该元素,这种运算称为索引运算。需要说明的是,[]
的元素位置可以是0
到N - 1
的整数,也可以是-1
到-N
的整数,分别称为正向索引和反向索引,其中N
代表列表元素的个数。对于正向索引,[0]
可以访问列表中的第一个元素,[N - 1]
可以访问最后一个元素;对于反向索引,[-1]
可以访问列表中的最后一个元素,[-N]
可以访问第一个元素,代码如下所示。
1 | items8 = ['apple', 'waxberry', 'pitaya', 'peach', 'watermelon'] |
在使用索引运算的时候要避免出现索引越界的情况,对于上面的items8
,如果我们访问items8[5]
或items8[-6]
,就会引发IndexError
错误,导致程序崩溃,对应的错误信息是:list index out of range,翻译成中文就是“数组索引超出范围”。因为对于只有五个元素的列表items8
,有效的正向索引是0
到4
,有效的反向索引是-1
到-5
。
如果希望一次性访问列表中的多个元素,我们可以使用切片运算。切片运算是形如[start:end:stride]
的运算符,其中start
代表访问列表元素的起始位置,end
代表访问列表元素的终止位置(终止位置的元素无法访问),而stride
则代表了跨度,简单的说就是位置的增量,比如我们访问的第一个元素在start
位置,那么第二个元素就在start + stride
位置,当然start + stride
要小于end
。我们给上面的代码增加下面的语句,来使用切片运算符访问列表元素。
1 | print(items8[1:3:1]) # ['strawberry', 'durian'] |
提醒:大家可以看看上面代码中的最后一行,想一想当跨度为负数时,切片运算是如何访问元素的。
如果start
值等于0
,那么在使用切片运算符时可以将其省略;如果end
值等于N
,N
代表列表元素的个数,那么在使用切片运算符时可以将其省略;如果stride
值等于1
,那么在使用切片运算符时也可以将其省略。所以,下面的代码跟上面的代码作用完全相同。
1 | print(items8[1:3]) # ['strawberry', 'durian'] |
事实上,我们还可以通过切片操作修改列表中的元素,例如我们给上面的代码再加上一行,大家可以看看这里的输出。
1 | items8[1:3] = ['x', 'o'] |
两个列表还可以做关系运算,我们可以比较两个列表是否相等,也可以给两个列表比大小,代码如下所示。
1 | nums1 = [1, 2, 3, 4] |
说明:上面的
nums1
和nums2
对应元素完全相同,所以==
运算的结果是True
。nums2
和nums3
的比较,由于nums2
的第一个元素1
小于nums3
的第一个元素3
,所以nums2 >= nums3
比较的结果是False
。两个列表的关系运算在实际工作并不那么常用,如果实在不理解就跳过吧,不用纠结。
元素的遍历
如果想逐个取出列表中的元素,可以使用for-in
循环的,有以下两种做法。
方法一:在循环结构中通过索引运算,遍历列表元素。
1 | languages = ['Python', 'Java', 'C++', 'Kotlin'] |
输出:
1 | Python |
说明:上面的
len
函数可以获取列表元素的个数N
,而range(N)
则构成了从0
到N-1
的范围,刚好可以作为列表元素的索引。
方法二:直接对列表做循环,循环变量就是列表元素的代表。
1 | languages = ['Python', 'Java', 'C++', 'Kotlin'] |
输出:
1 | Python |
总结
讲到这里,我们可以用列表的知识来重构上面“掷色子统计每种点数出现次数”的代码。
1 | """ |
上面的代码中,我们用counters
列表中的六个元素分别表示 1 到 6 点出现的次数,最开始的时候六个元素的值都是 0。接下来,我们用 1 到 6 均匀分布的随机数模拟掷色子,如果摇出 1 点,counters[0]
的值加 1,如果摇出 2 点,counters[1]
的值加 1,以此类推。大家感受一下,由于使用了列表类型加上循环结构,我们对数据的处理是批量性的,这就使得修改后的代码比之前的代码要简单优雅得多。