Python进阶-简单数据结构

本文为《爬着学Python》系列第九篇文章。


从现在开始算是要进入“真刀真枪”的Python学习了。之所以这么说,是因为学完Python进阶模块以后,基本上就具备了一定的Python编程能力了。可以说我们可以用不那么聪明的方法实现我们要实现的一切功能,而更高级的技巧则会在Python精进中介绍。

本文的目的在于介绍Python的内置简单数据结构,重点是列表list字典dict,也会简单介绍元祖tuple和集合set。我所说的“简单”,不是指这些数据结构学习起来难度低、或者说功能比较单一(其实相反),而是说这些数据结构在Python中最常用,形式最简单,而我们要介绍的又是他们的一些简单用法。

列表 list

任何了解过数据结构的人都应该明白,列表永远是应用最广泛,功能最多样化的数据结构,这样的数据结构是不应该称之为“简单”的。Python中的列表尤是如此。Python中的列表是一种分离式动态顺序表,可以实现非常多的数据结构,如可以直接作为,可以实现队列,可以直接表示完全二叉树所以可以作为使用,可以嵌套表示一般形结构,用自定义类进行简单改造能更加高效地实现以上的以及其他许多功能……

在介绍完列表的基础知识以后,会对上面这些结构的实现进行简单示例。

创建列表

Python中对于列表的使用就像普通变量一样,不需要变量声明,直接将列表元素用方括号[]框起来进行赋值即可。例如

>>> sample1 = [1, '2', 'woo!']

需要注意的是Python的列表可以同时接受不同类型的元素,非常宽容,无论是数字、布尔值还是字符串、其他变量、其他数据结构,甚至是函数,只要是对象,都可以作为列表元素保存在列表中。这也给Python列表操作带来了各种可能性,应用场景中充分发挥想象力可以使代码非常优雅简洁。

另外,我们可以对一切可迭代对象使用内置函数list()来获得一个新的列表(在下一篇自定义函数文中,我们会仔细讨论为什么说这是个新列表)。

>>> sample2 = list(sample1)

对于一般可迭代对象,我们还可以用列表生成器来完成生成列表的工作(关于迭代器和生成器会在更靠后的文章中进行介绍)。

>>> sample3 = [i for i in range(10) if i%2==0]

至此我们介绍了三种新建列表的方法:

  • 一般赋值语句
  • list() 函数转换可迭代对象
  • 列表生成器

在文末介绍元祖以后,我们会认识到其实这些方法都是殊途同归的。

列表的一些基本操作

我们提到过Python的内置列表是一种分离式动态顺序表,我们可以先不用知道这是什么意思,只要知道这代表Python列表是可索引的而且可以几乎无限制地往里面插入数据(如果以后确定开数据结构模块内容,会介绍实现方式)。

索引和切片

所谓的索引,可以理解为Python的列表元素按顺序有自己的“号码“,我们可以通过这个号码来直接访问Python的列表元素。

>>> sample1 = [1, '2', 'woo!']
>>> elem1 = sample1[2]
>>> elem1
'woo!'

注意到我们可以把这个列表元素对象通过赋值直接传给变量。而且,我们索引的号码是[2],得到的却是列表的第三个元素。原因是,列表元素的索引是从0开始的,它代表列表的第一个元素。而出现这种现象的原因,是因为“传统”。在许多较早出现的语言如C语言中,定义列表变量类型时需要声明,声明时有时候需要声明它的容量,这时候一般会写成int a[10],这表示声明一个能保存10个整数的列表。这时候再去试图用a[10]来访问其中的元素显然是会产生歧义的,事实上a[10]在C中代表这个列表本身。为了消除歧义也为了计算机处理方便,从0开始的索引就应运而生了,这其实也算是非常巧妙的设计,是工程师智慧的结晶。

在Python中虽然已经没有这样做的必要了,但还是遵循了这一传统,这也是为了方便早期的程序员更快地适应这种新语言。因此我们在以后也可能会看到有些Python面向对象程序设计会把列表的第一个元素设置为a[0]=0,然后用一个变量来保存真正在列表后面添加的元素个数,可以先定义currentsize = 0每次插入数据就currentsize += 1,或者每次需要访问长度时currentsize = len(a)-1。这是为了让列表更加符合逻辑而且在某些条件判断处理上有许多优势,否则也可以用a[0]来保存列表的真实长度。GitHub上有一部分Python程序员会采用这种方法,初次接触不要奇怪人家为什么要这么做。

好吧,说了这么多只是为了加深印象,列表元素的索引是从0开始的。另外一点要说的是,Python列表接受负数作为参数从列表尾端访问元素,不一样的是,列表尾端第一个元素的索引是-1,后面以此类推。

>>> sample1 = [1, '2', 'woo!']
>>> elem1 = sample1[-2]
>>> elem1
'2'

接下来讲列表元素的切片。在知道了索引的基础上,切片非常好理解。索引是访问列表中的一个元素,而切片则是访问列表中的一段元素。

>>> sample3 = [i for i in range(10) if i%2==0]
>>> slice1 = sample3[1:3]
>>> print(slice1)
[2, 4]
>>> print(type(slice1))
<class 'list'>

注意到切片返回的是一个列表。列表切片可以接受两个参数,一个作为起点,一个作为终点,其中终点不包括在切片范围中。这种特性是不是有点像range函数呢?其实,列表切片还有一些其他技巧。

>>> sample3 = [i for i in range(10) if i%2==0]
>>> sample3[1:]
[2, 4, 6, 8]
>>> sample3[:-2]
[0, 2, 4]
>>> sample3[::-1]
[8, 6, 4, 2, 0]

注意到

  • 切片时可以略去某端的参数,代表取到这一端的列表尽头。
  • 切片时也可以用负数当参数,规则和索引类似。
  • 切片可以和range函数一样接受第三个参数作为步长,这个步长也可以是负数,[::-1]这样的索引方式可以方便地将列表倒转。

在以上索引和切片的操作中还有一点要提的就是,如果索引的标签超出列表范围,程序就会抛出IndexError索引错误。

列表的加法和乘法

Python中的列表可以使用加法和乘法。那么为什么不能用减法和除法呢?因为索引和切片可以看作是使列表缩短的操作,这完成了减法和除法应该完成的任务。

Python列表的加法必须是两个列表进行相加,效果是将两个列表连接起来。

>>> sample1 = [1, '2', 'woo!']
>>> sample3 = [i for i in range(10) if i%2==0]
>>> sample1 + sample3
[1, '2', 'woo!', 0, 2, 4, 6, 8]

Python列表的乘法必须是列表乘以一个整数,效果是将该列表重复。

>>> sample1 = [1, '2', 'woo!']
>>> sample1 * 2
[1, '2', 'woo!', 1, '2', 'woo!']

这两个操作比较简单,不多赘述了。自此我们掌握了一般的使列表长度变化的方法。接下来我们需要认识的是Python列表的一些简单的其他内置方法。

列表的一些内置方法

这里涉及的都是一些简单内置方法,包括insert,len,append,pop,reverse,sort, clear。

insert和len

insert方法能够将新元素插入到列表的指定位置,接受两个参数,第一个是待插入的元素所插入位置的索引,第二个是该元素对象。

>>> sample3 = [i for i in range(10) if i%2==0]
>>> sample3
[0, 2, 4, 6, 8]
>>> sample3.insert(3, 1)
>>> sample3
[0, 2, 4, 1, 6, 8]

注意到插入时,将新元素插入要插入的索引位置后,从该位置起往后的列表元素都依次往后推了一格,索引加一。值得一提的是insert方法的第一个参数给定插入索引位置一样可以是负数,但是使用得不多。由于会把该位置原来的元素往后挤,所以即使是用-1当作参数,新元素在操作结束后是列表的倒数第二个元素并不是倒数第一个。

len方法会返回列表的元素个数,或者说叫列表的长度

>>> sample3 = [i for i in range(10) if i%2==0]
>>> len(sample3)
5
>>> sample3.insert(3, 1)
>>> len(sample3)
6

len方法堪称是列表操作中最常用的方法,简单实用。

在这里再介绍一个不是很常用但有时也有奇效的count方法,它接收一个参数,返回该参数在列表中出现的次数。这里只是为了和len方法进行区分所以就不作演示了。

append和pop

append方法可以在列表尾端插入元素,弥补了insert函数的不足。而且远远不只是这样,append操作的速度要比insert快上非常多。原因也是显而易见的,append操作不会挤到列表中的元素所以不用更改其他元素的索引。对应的pop函数会删除列表尾端的元素,并且返回该元素对象。

>>> sample3 = [i for i in range(10) if i%2==0]
>>> sample3
[0, 2, 4, 6, 8]
>>> sample3.append(1)
>>> sample3
[0, 2, 4, 6, 8, 1]
>>> sample3.pop()
1
>>> sample3
[0, 2, 4, 6, 8]

之所以要把这两个方法放在一起讲,是因为这两个方法的存在使得Python列表天然地可以直接作为使用。栈是一种后进先出的缓存结构,在计算机中应用非常普遍。诸如递归、函数嵌套等,都是通过栈实现的。如果以后开了数据结构板块我们会详细介绍栈的强大。

reverse,sort和clear

reverse方法会将列表反转。sort方法可以对列表元素进行递增的排序,加入参数reverse=True则可以进行递减的排序。clear方法会清空列表元素。

>>> sample3 = [i for i in range(10) if i%2==0]
>>> sample3
[0, 2, 4, 6, 8]
>>> sample3.reverse()
>>> sample3
[8, 6, 4, 2, 0]
>>> sample3.sort()
>>> sample3
[0, 2, 4, 6, 8]
>>> sample3.clear()
>>> sample3
[]

在以上涉及到改变列表长度的操作中, Python的处理办法是容量不足时列表等量扩容,但是容量冗余时不会自动清除多余的容量。还是那句话,如果仔细讨论Python数据结构的话,会对相关内容进行详细说明。

列表是“可变”对象

介绍完列表的一些基本操作以后,我们要讲一个比较重要的知识点。这是Python可变对象的一些特性,是面试时几乎必然会涉及的内容。我们在此处先简单介绍一下,在下一篇文章自定义函数相关内容中会更加仔细地介绍Python中可变对象的特性为什么会这么重要。

首先我们要明确的一点是,列表是可变对象。在上面我们介绍过的一系列操作,都是对列表对象本身进行操作。列表应该是我在本专题中第一次介绍可变对象的概念。我们没接触过可变对象没关系,我们接触过不可变对象。

>>> a = 3
>>> b = a
>>> a = a + 1
>>> print(b)
3

在上面的操作中我们对a赋值为3,再将a的值赋给b,再将a的值加1,这时我们输出b的值,b还是3。这一切看上去是理所当然的。那我们试一下对列表进行这样的操作呢?

>>> a = [3]
>>> b = a
>>> a = a + [1]
>>> print(a)
[3, 1]
>>> print(b)
[3, 1]

在上面的操作中我们对a赋值为单一元素的列表[3],再将a的值赋给b,再将列表a后面接上一个新列表[1]。根据刚才我们介绍过的列表操作可知,现在列表a应该变成了[3, 1]。试一下输出a,确实是这样的。但这时我们输出列表b,b还会是[3]吗?事实上不是的,现在列表b也变成了[3, 1]

这就是这个知识点重要的原因,我们没有对列表b进行操作,可是它却改变了。如果在实际项目中经常出现这样的现象,那恐怕bug多得永远也清不完。这要求我们要时刻清楚地意识到Python对象的特性。这里先简单说一下会发生这种现象的原因,具体的细节以及正确的操作方法在下一篇文章会重点介绍。

简单来说就是对于可变对象,它的许多方法会修改这个对象本身,而对于不可变对象则是重新创建一个新对象。我们将值为3的a加1,事实上是将a指向了一个值等于3+1的新对象。而我们在列表[3]后面接上列表[1],它则是把列表[1]中的元素依次放进列表[3]的尾端。这两种操作的区别是,前者创建了新对象而后者没有,后者只是对原对象进行修改。因此如果a和b都是指向这个列表,那么这个列表变动时,a和b都会同时受到牵连。

这种特性要求我们分辨清楚,哪些操作是针对可变对象本身的,哪些操作是将可变对象作为参数参与运算而不进行修改的。如果我们不能合理地规避那些不应该出现的修改,那么将会有无尽的奇怪bug出现在我们的程序中。而分辨方法比较简单,就和之前我们分辨函数变量和函数对象类似。如果列表对象出现在赋值语句的左边,可以认为是对列表变量进行修改,如果在右边则要分情况,如切片和索引就只访问列表但不做修改,但是像resort方法pop方法就会修改列表了。

相信看到这里你大概能理解为什么我把标题取为“简单数据结构”,单是列表,在绝大部分内容我们都是浅尝辄止的情况下,已经是非常大的篇幅了。我只能浅显地介绍一下相关内容,更多内容在后面的学习过程中再继续深入介绍。

字典 dictionary

字典顾名思义,就像字典一样每个字对应一段解释文字,Python中的字典也是非常典型的以逗号分隔以冒号分割的键值对数据结构,形式为:{键1:值1, 键2:值2, 键3:值3,}。和列表类似,字典的键和值对于赋值对象是比较包容的。而且注意到我们在字典的最后一个键值对后面还是加上了逗号。

这个逗号不是必须的,列表中我们不一定会这么做,但是在字典中我推荐这样做。原因和字典的嵌套有关。

sample_dict = {key1:value1,
               key2:{value2_key1:value2_value1,
                     value2_key2:value2_value2,
                     value2_key3:{value2_value3_key1:value2_value3_value1,
                                  value2_value3_key2:value2_value3_value2,
                                  value2_value3_key3:value2_value3_value3,
                                  value2_value3_key4:value2_value3_value4,
                                 },
                     value2_key4:value2_value4,
                    },
               key3:value3,
               key4:value4,
               }

不要被上面的这个字典吓到。事实上我们真正遇到的字典往往就是比这个还要复杂,但嵌套起来其实也很直观,一目了然。我们以后处理json数据的话就会将其转换为Python字典进行操作。注意到由于每个元素后面都有逗号,我们可以轻松地在某个元素后面回车来进行插入新的键值对的操作。也可以简单地理解为,这样的话字典的每个元素就变成了“键:值,”,从第一个元素到最后一个元素都是这样,而我们在尾端按这种格式添加字段的时候,这种结构不会被破坏。

当然了,就和逗号后面加空格一样,这种事情做不做一般不会影响程序的执行,但是我们应该养成好习惯,提高代码可读性,减少出错的机会。或者如果使用的字典很简短,那么也可以不打最后这个逗号。

字典的键值对结构给其带来了列表无法比拟的强大之处。它能完成信息检索的功能,这是非常实用的功能,它可以轻松的建立映射结构。字典可以轻松地胜任数据结构,这对列表来说非常吃力。字典可以完成表驱动的一系列操作,使程序简洁优雅,思路清晰。另外,Python的字典是基于散列实现,这使得它的检索速度非常快。字典相关的深入的数据结构知识都是一些高级数据结构相关内容,这些知识都非常有趣,我会认真考虑是否有必要开数据结构专题的。

字典的索引

字典有类似于列表的索引访问方式。字典[键]这种形式就可以访问该键对应的值。

>>> a = {'b':'c', 'd':'e',}
>>> a['b']
'c'
>>> a.get('b')
'c'

我们这里顺便介绍一下get方法,我们看到它和索引类似返回了字典键对应的值,那么它和索引有什么区别呢。

区别在于,我们如果用一个不存在的键去索引就会和引发和列表IndexError索引错误类似的KeyError键错误。但是get方法不会,如果在字典中找不到对应的键,则会返回一个None。那是不是get方法就一定比索引好用呢,也不是的。

因为字典的索引还可以用来赋值,修改字典或者添加键值对。当索引的键是字典中已经存在时,赋值语句会修改该键对应的值,如果字典中并不存在这样的键,则会新建一个键值对加入字典中。

字典遍历

字典的遍历主要分两种,键和值的遍历。首先字典本身是可迭代的,迭代的对象是字典中的键。

>>> a = {'b':'c', 'd':'e',}
>>> for i in a:
...     print(type(i), i)
...
<class 'str'> b
<class 'str'> d

这样做有一种好处,我们得到了键,也就能用索引访问它对应的值,这样也就间接遍历了字典中的值。说到底,字典的迭代的基本单位应该是键值对,但是Python把键作为键值对的代表,增加的操作的灵活性。

>>> a = {'b':'c', 'd':'e',}
>>> for i in a:
...     print(a[i])
...
c
e

除了直接遍历,我们也可以使用字典的内置方法来直接得到我们需要的结果

>>> a = {'b':'c', 'd':'e',}
>>> a.keys()
dict_keys(['b', 'd'])
>>> a.values()
dict_values(['c', 'e'])
>>> a,items()
dict_items([('b', 'c'), ('d', 'e')])

需要说明的是这三个内置函数返回对象虽然不是列表,但都是可迭代对象,这意味着我们我们也可以用这些来遍历字典。

>>> a = {'b':'c', 'd':'e',}
>>> for i in a.items():
...     print(type(i), i)
...
<class 'tuple'> ('b', 'c')
<class 'tuple'> ('d', 'e')

注意到items()方法的迭代对象是一个(键, 值)两个元素组成的元组。元祖是我们接下来马上要介绍的一种数据结构。而且其实我们之前用的直接对字典遍历for i in a其实就相当于for i in a.keys()

字典的一些内置方法

其实在上面我们已经接触了一些字典的内置方法,他们都是一些访问字典内容的方法,这里我们要讲的主要是poppopitem这两个从字典中弹出元素的方法。

我们在介绍列表时已经接触过pop方法了,但是字典的pop方法不一样,字典的pop方法需要指定一个键作为参数,返回值为该键在字典中对应的值。

>>> a = {'b':'c', 'd':'e',}
>>> a.pop('b')
'c'
>>> a
{'d': 'e'}

除了用pop方法外,也可以用诸如del a['b']这样的形式用内置方法从字典中删除指定键值对,但是不同的是不会有返回值。事实上支持索引的可变数据类型基本上都支持del方法。

那么这个popitem方法又是用来干什么的呢。其实字典的popitem方法倒更像列表的pop方法,注意只是“像”,这两者还是有区别的。

>>> a = {'b':'c', 'd':'e',}
>>> a.popitem()
('d', 'e')
>>> a
{'b': 'c'}

注意到字典popitem方法和列表pop方法一样不需要参数,会弹出一个元素作为返回值。这个返回值的形式和items方法的迭代对象类似,是个两个元素的元组。但这不是我要说的区别,我要说的区别是,Python列表是顺序表,所以它能弹出最后一个元素,但是Python字典的键值对的顺序其实是不可靠的,不要寄希望于popitem方法来弹出最后插入的键值对,可以认为popitem方法是从字典中随机弹出一个键值对。

要想按插入顺序后入先出弹出字典元素可以使用collections中的OrderedDict,文末会与其他标准库补充数据类型一起进行更多讨论。

由于有了介绍时列表建立的一部分基础,字典理解起来和列表进行对比记忆就行了。还是那句话,更深入的应用会在可能出现的数据结构部分讲解,本文目前更需要关注的是理解这些数据结构的特点与简单的使用方法,和在实际操作中去熟悉这些数据结构。

元组 tuple 和 集合 set

元组tuple和集合set是Python中的另外两个常用数据结构。我们在上文中强调过列表list是可变对象,可以把元组tuple简单理解为不可变的列表。这意味着列表支持的许多操作元组是不支持的,但这同时也使得元组比列表安全许多。因此,在Python许多内置操作和函数中,元组的应用反而要比列表更加广泛。

我们说可以把元组tuple简单理解为不可变的列表,类似的,集合set可以简单理解为不含重复元素的列表。

创建元组

元组创建方式和列表类似,但是符号改成圆括号。

>>> a = ('b', 'c', 'd')
>>>type(a)
<class 'tuple'>
>>> b = 'c', 'd', 'e'
>>> type(b)
<class 'tuple'>
>>> print(b)
('c', 'd', 'e')

注意到我们还使用了一种直接定义方式, 当我们在赋值语句的等号右边用逗号分隔多个对象时,如果等号左边只有一个变量,那么这些对象将会作为元组的元素赋值给该变量。和字典一样,元组的逗号也有一点规定,单个元素的元组,元素后面必须加上逗号

在这里我们要把定义列表的方法再拿出来说一说,我们是不是可以认为列表的一般赋值语句sample_list = [elem1, elem2]是相当于对元组对象(elem1, elem2)[]的形式使用了Python内置的list函数将其转换成列表呢?

是也不是。Python的列表生成操作中[]或者list(iter)其实是用于任何可迭代对象的函数。而我们见到的b = 'c', 'd', 'e'赋值直接把其作为元组保存是因为,Python中元组比列表是更优先的元素存储数据结构。这也是我说Python中元组可能比列表反而应用更广泛的原因,元组是Python中默认的最基本可迭代对象形式。关于赋值语句右端用逗号分隔的对象是可迭代对象,我们会在下一篇函数参数相关知识中进行讲解。

而元组比列表更被Python推崇的原因恰好就在于,元组是不可变的对象。这使它被认为是更“安全”的。这也是我们在上文中花大量篇幅说列表是可变对象的原因之一,元组与列表最大的区别就在此。

元组的基本操作

元组可以像列表一样索引和切片,可以用加法和乘法,可以用len获取长度。但是列表那些改变自己本身的操作元组是不支持的,比如说pop和append或者像sort和reverse方法。即使元组也是有序的,但它不能做这些事情。

但是元组的元素排序不是做不到的,我们用sorted(iter)这个内置函数就可以了。这个函数就像list(iter)函数一样可以对任何可迭代对象使用,但是返回结果是列表。我们可以对返回结果使用tuple()函数再赋值给某个变量就可以了。当然,你也看出来了,这有点蛋疼,还是直接用列表吧。

那么什么时候该用列表,什么时候该用元组呢。我的建议是,当元素类型单一结构相似而变化范围较大的时候使用列表,这样可以方便我们进行操作。而元素类型复杂或者元素类型单一但是元素相对确定的时候,我们可以使用元组作为变量保存池。举例来说星期一到星期天这七天更适合存储在元组中,而我们的待办事项更适合存储在列表中。当然,如果要将星期和待办事项进行对应,我们应该使用字典。

说到字典。我们上文中提到字典对存储对象比较宽容,而讨论列表时我们用的却是非常宽容。现在我们看看为什么字典不那么宽容。我们提到过Python字典某种程度上其实可以看作是哈希表,这要求Python字典的键是常量,也就是我强调过的不可变对象,因此,元组可以作为字典中的键,但是列表不可以。

在下一篇函数中,我们还会遇到元组和列表对比相关的问题。

集合

我并不打算对集合展开太多的内容。就像之前所提到过的,我们只要将其理解成不包含重复元素的列表即可。不过需要注意的是集合中的元素是无序的,因此集合不支持索引。在这里只简单介绍一下集合的定义方式。

>>> a = {'b', 'c', 'd', 'd'}
>>> print(type(a), a)
<class 'set'> {'c', 'd', 'b'}
>>> a = {}
>>> type(a)
<class 'dict'>

注意到集合在定义时会自动删除重复的元素。集合的符号和字典类似,但是我们定义空集合时不能使用{},这样得到的是一个空字典。我们可以用定义集合的另一种方式定义空集合,Python中对可迭代对象可以像列表和元组一样使用set()函数来生成一个新集合。

>>> a = set()
>>> print(type(a), a)
<class 'set'> set()
>>> a.add('b', 'c')
>>> a
{'b', 'c'}
>>> a.add('c', 'd')
>>> a
{'b', 'c', 'd'}

注意到我们使用add方法向集合中添加了元素,这说明集合也是可变对象。而且不管我们使用什么样的方法,集合中永远不会出现重复的元素。

另外值得一提的是集合之间的操作,即数学意义上的交、并、补、异或,它们对应的操作符分别是&, |, -, ^

Python其他内置数据结构

在Python的标准库collections中还有许多适应不同需求的其他数据结构,常见的比如deque用来比列表更好地完成堆栈队列的功能,Counter用来以字典的形式保存列表中元素出现的次数,OrderDict用来构造有序的字典,defaultdict用来构造有默认值的字典,以及其他一系列的用来构造自定义复杂数据结构的基础数据类型类。

这些数据结构应用比较灵活,在以后的实际操作中偶尔会接触到他们。在这里我也不打算展开来介绍了。

最后的废话

讲到这里我想要讲的有关Python简单数据结构内容就基本结束了。我们把主要精力放在了列表上。我们通过列表熟悉了数据结构是用来做什么的,大概会具有什么样的功能,因为列表是功能比较齐全,比较典型而且应用广泛的数据结构。这样我们理解元组和集合比较简单,一个可以简单理解为不可变列表,一个可以理解为不含重复元素的无序列表。另一个我们介绍比较详细的是字典,这种数据格式是互联网信息交互中应用最广泛的类似数据形式。

最后再说明一下为什么我会犹豫要不要详细讲Python中的复杂数据结构,开个专门的板块来介绍。毕竟在我计划中字符串都是有单独板块的。

这是因为我认为Python中数据结构没那么重要。我觉得Python的学习非常简单,一个是面向对象的理念要落实,一个是放开思路灵活运用第三方库。前一个帮助我们理解别人的代码到底是什么意思,后者能够帮助我们快速地解决问题。一切编程方法,无论是面向这个面向那个,基于这个基于那个,说到底是基于计算机面向问题编程。能解决问题才是关键

因此,如果有数据结构基础,那么完全没有必要学习如何用Python构造树和图或者哈希表。简单的就简单用deque完成栈队功能或者使用字典即可,复杂的可能说你思路需要修改或者说根本不适合用Python来完成复杂数据结构。

这样说可能显得我根本不需要犹豫,直接略过数据结构就可以了。但是不是这样的。我觉得任何想要深入编程技术的人,都应该学习数据结构,应该看一下优秀的前辈是如何解决问题的。为什么他们可以想出这样的数据结构完成这样的功能。再一点,用Python来学习数据结构,刚好就是熟悉Python面向对象的最好方式。我们也可以通过学习比较看出Python面向对象时与其他语言的异同点。

在这里先在文末给大家推荐两个通过学习数据结构的途径。一个是在线的英文网站,文章内容很高,而且支持实时操作测试,如果没有语言障碍会是非常好的学习体验,强烈推荐。另一个是我看过的一本实体书,北大老师写的《数据结构与算法:Python语言描述》。我直言不讳地讲里面的代码质量不高,很多地方值得改进,对Python特点理解得也不算透彻,很多代码可以明显看出C的痕迹。但是这本书相对简单,很扎实,讲解也比较详尽,对于初学者来说也算不错的学习材料。如果要学习系统的数据结构与算法的知识,coursera上有大量的斯坦福和普林斯顿等国外高校的优质课程,这里就不贴链接了,会科学上网不至于不知道coursera。

链接

  1. Problem Solving with Algorithms and Data Structures using Python — Problem Solving with Algorithms and Data Structures
  2. 数据结构与算法:Python语言描述 (豆瓣)

推荐阅读更多精彩内容