当前位置: 首页 > 后端技术 > Python

《对比Python学围棋》——数据结构进阶

时间:2023-03-26 00:52:45 Python

本文是《对比Python学围棋》系列文章的第四篇。在本文中,我们将了解Go的高级数据结构。因为文章太长,分两篇。这是上一篇文章。本系列的其他文章,可以前往《对比Python学Go》——开篇查看,开始今天的分享。Python数据结构底层完全依赖解释器的实现,文中数据结构对应默认解释器CPython没有特别说明。在数据结构方面,有“数组”和“链表”两种基本数据结构,还有很多基于它们的高级数据结构,如栈、队列、哈希表等。作为编程语言,Go和Python如何定义自己的数据结构?根据数据结构的特点,我们可以大致将Go和Python的数据结构分为以下几种类型:“类数组结构”,具有数组的一些特征,但又不完全是数组。“Hash类型”,即key-value类型或者map类型。一些特定于语言本身的高级构造。下面就来一一介绍吧。类数组结构的数组,是线性表结构。它具有以下特点:使用一组连续的内存空间来存储数据。存储的数据属于同一类型。回顾完数组的特性,我们再来看看Go和Python中类数组的数据结构。Go在Go语言中,有两种类似数组的数据结构,“数组”和“切片”。ArraysGo的数组属性可以概括如下:固定长度:这意味着数组不能增长或收缩。要扩展一个数组,只能新建一个数组,将原数组的元素复制到新数组中。内存连续:这意味着数组中的元素可以通过下标(arr[index])进行索引。固定类型:固定类型是指每个数组元素可以存储什么数据,每个元素可以存储多少字节数据。数组的初始化和操作如下:packagemainimport"fmt"funcmain(){//类型[n]T是一个数组,有n个T类型的值//先声明,再赋值vara[2]stringa[0]="Hello"a[1]="World"//声明时,直接赋值b:=[5]int{10,20,30,40,50}//可以直接访问数组通过下标fmt.Println(a)fmt.Println(a[0],a[1])fmt.Println(b)fmt.Println(b[1],b[2])//数组可以是通过len()函数得到Lengthfmt.Println(len(a))fmt.Println(len(b))//数组元素赋值b[1]=25fmt.Println(b)fmt.Println(b[1])}除了普通数组之外,还有多维数组,也就是数组的数组。//声明一个二维整型数组,两个维度分别存放4个元素和2个元素vararr[4][2]intarr[0]=[2]int{10,11}arr[0][1]=15//声明时,直接赋值arr1:=[4][2]int{{10,11},{20,21},{30,31},{40,41}}//声明时,下标赋值arr2:=[4][2]int{1:{20,21},3:{40,41}}arr3:=[4][2]int{1:{0:20},3:{1:41}}//使用数组类初始化数组vararr4[2]int=arr1[1]//使用数组元素赋值普通元素varvalueint=arr1[1][0]//使用index获取数组值fmt.Println(arr)fmt.Println(arr1)fmt.Println(arr1[0][1])fmt.Println(arr2)fmt.Println(arr3)fmt.Println(arr4)fmt.Println(值)//[[1015][00][00][00]]//[1015]//[[1011][2021][3031][4041]]//11//[[00][2021][00][4041]]//[[00][200][00][041]]//[2021]//20数组中未初始化的元素会被自动初始化为每个类型对应的“零值”。分片后的围棋数组长度是固定的,数据值存放在内存空间中。当存储量特别大时,使用起来极为不便。在Go语言中,除了数组之外,还定义了一种类似数组的结构,称为“切片(slice)”。切片是数组段的描述符。它由指向底层数组的指针、数组段长度及其容量(段的最大长度)组成。切片更像是数组的引用类型,而数组是值类型。其结构如下:在Go编程中,由于数组的各种限制,在处理数据集合类型时,切片是首选的数据结构。切片的底层数据存储仍然是一个数组,所以切片也具有数组的一些特性。//使用make(slice,len,cap)声明切片。上限可以省略。省略时,等于lensli:=make([]int,2,3)fmt.Println(sli)//不赋值时,每个元素都对应零值fmt.Printf("%p%v%v\n",sli,len(sli),cap(sli))//声明时,初始化nums:=[]int{10,20,30,40}fmt.Println(nums)fmt.Printf("%p%v%v\n",nums,len(nums),cap(nums))//nilslice,指针为空,长度和容量均为0varnums1[]intfmt.Println(nums1)//空slice,the指针指向一个空数组,长度和容量为0varnums2=make([]int,0)nums3:=[]int{}fmt.Println(nums2)fmt.Println(nums3)nums[0]=12fmt。Println(nums)//从一个slice创建一个slicefmt.Println(nums)fmt.Println(nums[1:2])//长度为2-1=1,容量为1fmt.Println(nums[1:2:4])//长度为2-1=1,容量为4-1=3//slice[i:]//从i切到末尾//slice[:j]//从头切到j(不包括j)//slice[:]//从头到尾剪切,相当于复制整个slice//sliceappend,使用内置functionappend(src,item)returnnewslicenums=append(nums,10)//添加一个nums=append(nums,10,20)//同时添加多个newNums:=append(nums,nums1...)//合并两个切片fmt.Println(newNums)fmt.Println(nums)//复制切片,使用内置函数copy(dst,src)返回复制的元素个数//赋值时,接收到的切片的容量必须大于原始切片,否则复制失败不报错copyNums:=make([]int,5)count:=copy(copyNums,nums)fmt.Println(count)fmt.Println(copyNums)fmt.Println(nums)上面说了,Go的slice基本上是一个数据段的描述符,底层是引用的数组结构。当多个切片同时引用一个数组时,使用下标修改切片会相互影响。使用时一定要注意。//数组的共享切片sli:=make([]int,3)//定义长度和大小为3的切片sli1:=sli[:2]//从切片、sli和底部引用了sli1同一个数组//slice:0xc0000160c0,len:3,cap:3fmt.Printf("slice:%p,len:%v,cap:%v\n",sli,len(sli),cap(sli))//slice1:0xc0000160c0,len:2,cap:3fmt.Printf("slice1:%p,len:%v,cap:%v\n",sli1,len(sli1),cap(sli1))sli[0]=10fmt.Println(sli)//[1000]fmt.Println(sli1)//[100]切片的容量可以在使用过程中动态增加。slice的动态增长是通过内置函数append()实现的,它会自动处理slice扩容和缩容的所有细节。扩容长度通常是原来分片容量的两倍,当容量大于1024时,增加量变成原来容量的1/4。//切片扩展fmt.Printf("%p%v%v\n",sli,len(sli),cap(sli))//0xc0000160c033sli=append(sli,1)sli=append(sli,2)fmt.Println(sli)//[100012]fmt.Printf("%p%v%v\n",sli,len(sli),cap(sli))//0xc00001a0c056以上code中间的sli长度为3,容量为3,append元素后,因为容量已经满了,所以容量自动翻倍。两次追加后,长度变为5,容量为6。除了长度和容量之外,你可能已经注意到数组的地址发生了变化,这意味着当append函数扩展时,它会创建一个新的底层数组,而不是直接追加和扩展原始数组。append函数扩展以创建一个新数组。这时候通过下标修改数据或者继续执行append都不会覆盖原来的底层数组,因为它已经不是数组了。此功能通常用于在从一个切片创建另一个切片时防止切片分配相互影响。sli:=make([]int,3)//定义一个长度为3的切片fmt.Println(sli)//[000]sli1:=sli[:3:3]//sli1的长度为3-0=3,容量为3-0=3fmt.Println(sli1)//[000]fmt.Printf("%p%v%v\n",sli,len(sli),cap(sli))//0xc0000160c033fmt.Printf("%p%v%v\n",sli1,len(sli1),cap(sli1))//0xc0000160c033sli1=append(sli1,2)//容量已满,新建底层数组fmt.Printf("%p%v%v\n",sli1,len(sli1),cap(sli1))//0xc00001a0c046PythonPython中类数组的数据结构是列表(列表)和元组。List列表比数组更高级。列表的底层结构如下:typedefstruct{PyObject_VAR_HEADPyObject**ob_item;Py_ssize_t已分配;}PyListObject;丢掉publichead变量PyObject_VAR_HEAD,列表由一个指针数组ob_item组成,以及分配的容量组成。结构如下:链表中并没有存储实际的值,而是存储了值的引用地址。引用地址可以指向任何类型的数据,这样就可以理解为什么列表中可以有任何类型的元素了。另一方面,链表中的引用地址大小相同,存储在一个连续的存储空间中,因此具有数组的一些特性,可以通过下标快速定位。根据列表的底层结构和Python官方文档HowarelistsimplementedinCPython?,得出List具有以下特点:列表元素可以使用下标索引取值,每个元素有位置顺序,并且底层是一个连续的存储空间。list里面存放的是数据的内存地址,并不是真正的数据。所以从上层结构来看,list列表可以存储任意类型,即list元素中的内存地址可以指向任意类型。可以任意添加新元素,采用“动态扩展”的策略不断添加新元素。扩张策略的增长倍数大致是这样的:0,4,8,16,24,32,40,52,64,76...,参考源码listobject.c。列表初始化和操作如下:#列表初始化l1=[]#推荐,更快l2=list()l3=[1,2,3,4]#列表添加print(l1+l2)print(l1*2)#你可以使用*复制列表l3+=l1print(l3)#列表值print(l1[2])print(l3[1:2])#从下标1到下标2print(l3[1:])#toendprint(l3[:2])#startat0#lengthoflistprint(len(l1))#deleteelementwithindex3dell1[3]Python的列表作为内置的高级数据结构,到实现一系列的操作功能。dir(list)['__add__','__class__','__contains__','__delattr__','__delitem__','__dir__','__doc__','__eq__','__format__','__ge__','__getattribute__','__getitem__','__gt__','__hash__','__iadd__','__imul__','__init__','__init_subclass__','__iter__','__le__','__len__','__lt__','__mul__','__ne__','__new__','__reduce__','__reduce_ex__','__repr__','__reversed__','__rmul__','__setattr__','__setitem__','__sizeof__','__str__','__subclasshook__','append','clear','copy','count','extend','index','insert','pop','remove','reverse','sort']l1=[1,2,3,4,5]l2=[6,7,8,9,10]#listappendl3=l1.append(6)#listmergel4=l1.extend(l2)#listelementindexprint(l1.index(2))#列表2的索引l1#插入列表元素print(l1)#[1,2,3,4,5,6,6,7,8,9,10]l1.insert(2,12)#插入index2位置的值为12,原值和后面的值将向后移动。print(l1)#[1,2,12,3,4,5,6,6,7,8,9,10]#弹出列表中指定索引的元素num=l1.pop()#默认是最大索引print(num)#10print(l1)#[1,2,12,3,4,5,6,6,7,8,9]num=l1.pop(3)#弹出索引为3的元素,下面向前移动元素print(l1)#[1,2,12,4,5,6,6,7,8,9]#删除指定值的元素print(l1)#[1,2,12,4,5,6,6,7,8,9]l1.remove(2)#删除值为2的元素,并将后面的元素向前移动print(l1)#[1,12,4,5,6,6,7,8,9]#清除列表print(l1)#[1,12,4,5,6,6,7,8,9]l1.clear()#清除列表l1print(l1)#[]#对列表进行排序print(l2)#[6,7,8,9,10]l2.sort()print(l2)#[6,7,8,9,10]l2.reverse()print(l2)#[6,7,8,9,10]除了列表自带的一些操作外,一些内置函数也适用。#sorted排序函数print(l2)l21=sorted(l2,key=lambdax:x,reverse=False)print(l21)tuple除了Python中的列表,元组更像是数组。元组类似于列表,只是它们不能添加、删除或修改。底层结构如下:typedefstruct{PyObject_VAR_HEADPyObject*ob_item[1];}PyTupleObject;除了头部字段,只有一个指针数组。没有像列表那样分配容量字段,体现了元组的不变性。除了元素的不可变特性外,其他的都像列表,是列表类型的子集。你可能会有这样的疑问,有列表,元组存在的意义是什么?与列表相比,元组有以下优点:因为元素是不可变的,所以可以作为哈希类型的键值。这使得对密钥的描述更丰富,更容易理解。对于元组,解释器会缓存一些小的静态变量使用的内存,这样初始化比列表更快。元组初始化及常用操作:#元组初始化a=(1,2,3)b=('1',[2,3])c=('1','2',(3,4))d=()e=(1,)#当元组只有一个元素时,需要在末尾使用逗号print(a,b,c,d,e)#(1,2,3)('1',[2,3])('1','2',(3,4))()(1,)#下标获取值print(a[1])#2#元组合并print(a+b)#(1,2,3,'1',[2,3])#内置函数使用#元组长度print(len(a))#3#使用*是复制指针f=a*2print(f)#(1,2,3,1,2,3)print(id(f[0]))#4376435920print(id(a[0]))#4376435920print(id(f[3]))#4376435920#无法更新编辑#a[0]=1#Traceback(最近调用最后):#文件“/Users/deanwu/projects/01_LearnDocs/learn_codes/python/python_list.py”,第15行,在#a[0]=1#TypeError:'tuple'objectdoesnotsupportitemassignment#Cannotdelete#dela[0]#Traceback(mostrecentcalllast):#File"/Users/deanwu/projects/01_LearnDocs/learn_codes/文件"/Users/deanwu/projects/python/python_list.py”,第21行,在#dela[0]#TypeEerror:'tuple'objectdoesn'tsupportitemdeletion总结在这篇文章中,我们学习了Go和Python的高级数据结构中类数组的结构,它们都有数组的一些特性,但是它们都有自己的语言特征。Go的slice和Python的list底层都是基于数组的,但是Go的slice更像是一个数组描述符指针,而Python的list使用地址和数据分开存储,引用地址存储在连续的空间中,继承数组是快速地。搜索的好处是外部存储实现了任意类型元素的存储。总的来说,它非常巧妙。不管是哪一种语言,我们在使用的时候,不仅要了解结构的基本用法,还要了解它的底层逻辑结构,这样才能避免在使用的时候出现一些莫名其妙的陷阱。进一步阅读Golang中的列表类型slicepythonCPython中列表的实现listobject.c我是吴院长,一个想成为真正的SRE的人。关注公众号“码农吴老师”,第一时间获取最新文章。回复关键字“go”“python”获取我收集的学习资料,也可以回复关键字“小二”,加我wx拉你进技术交流群,聊技术聊生活~