链表数据解构很重要,不亚于列表(有顺序连续的结构)
链表是有顺序但在内存中部连续的数据结构,每个元素离的比较远,习惯称为手拉手
链表和列表的区别
链表和列表的好处,链表的好处是增删比较方便,增删对于列表就比较麻烦
查找的时候,链表比列表慢,因为链表是手拉手,一个问下一个的,是比较耗时的,
linked list,链接的表,上一个结点知道下一个结点在哪里
对于双向的链表是,上一个知道下一个,下一个知道上一个,头和尾是两个特例,一个没有上一个,一个没有下一个
链表本身是个容器,容器里面装其他元素,其他元素自己知道下一个是谁,并不是知道链表下一个谁,每一个元素知道下一个是谁,链表是一个容器,元素里放的内容合法就行
加个头和尾
加元素append,首先需要把元素包装起来,node把item包装,就获得一个元素对象,不包装不行,直接放一个item不合适,每个元素放进来除了包装意外,还需要捆绑一个下一条是谁,next
现在拿到一个结点把它放进去,对于单向链表,追加相对来说比较简单,只要知道尾巴就可以解决了,留尾巴是代表找尾巴比较方便,不然给个开头,不知道尾巴在哪里,不然要迭代一遍才知道尾巴是谁
假设现在放第一个元素,如果一个元素都没有,追加元素相当于self.head=node,self。tail=noe,就一个元素没有上一条和下一跳
对于单向链表,head=none,说明是一个元素都没有,准备添加第一个元素进去
‘但凡head不是none,添加元素,就准备在tail上做文章 了’,当前尾巴的下一个,就是未来要加的,然后当前尾巴还要再修正一下
第一个元素,头尾都是记录5这个元素,tail和head是同一个东西,增加一个元素,tail修改了next
再添加3,把中间的每一次的下一跳都要修改上,tail指向3
第一次添加元素是当前尾巴,要指向下一个要加入的节点,下一跳解决之后,下一跳尾巴更新下就好
这两条语句顺序能否颠倒,颠倒了以后顺序就完全错了
这两句都有就可以注释掉,是一样的,在外面写self。tail=node
接下来要return,不要return就不太适合做链表,return self(这个时候已经调整完了,不遍历到最后怎么append)就代表把整个链表就返回了,就调整完了,在当前尾巴加元素,tail就少了很多遍历,链表追加还是经常使用的
self。head可以解决头部追加的问题,头部移除的问题,加个tail可以解决尾部追加和移除的问题,虽然链表可以找到某个元素之后把手断开重新拉手,但是链表虽然增删速度快,但是少不了要找一遍,找的时候挺慢的,所以链表也有不好的地方,但是加了tail属性之后,现在可以说,只要在头尾增删,效率是非常高的,链表头尾增删非常多
但是使用列表的时候如果头部增删还是少用,会引起整个链表的挪动
所以list使用的时候要特别小心增删的问题,只有尾部增删可以达到O(1)的效果
return self回去,就可以连续调用append
如何来迭代,for循环在python中是在有限次数场景使用的
就可以从头开始,current =self。head
当前头如果是none就没必要进去了
当前有一个元素就yield出去,yield是yield,差一步需要挪动
需要挪动,current=current.next
出现好几个说明递归了
如果下一跳不是none,则把item打印出来,下一跳是none,就没有item了,1下面是2,2下面是3
这么写更好的理解,1指向2,2指向3
这样单向链表就结束了,写链表的时候只要想没有数据的情况下,添加第一个节点怎么做,再想想,有 一个节点,2个节点该怎么做
链表和列表,在遍历的时候谁也快不了多少,因为要找下一个,再找下一个,任意找一个就要找半天了,这就是链表的问题
如果想要从里面拿出第5个元素,按照之前的,应该遍历此链表才能找到,但是能不能塞一张列表进来,列表找索引特别快,加一个这个是本身解决链表本身不太好做的事情
构建一个列表来辅助,遍历有两种方式看修改个写法,iter是给一个生成器,生成器直接包一下即可
也可以这么写,但是遍历都不会太快的,所有元素都要从里面拿出来
现在是借用这个列表来辅助你为它提速,这个所谓的提速,仅限于查找,如果链表增删,列表是帮不了什么忙的
前面代码不修改,就可以进行追加
这两个方法就提速了,这两个方法找起来就快多了,通过列表大大提升了链表
但是如果插入的话,列表不能嫌烦就不管了,链表和列表应该是一致的,仅当前来看,单向链表只满足append。iternodes方法,目前来看是没有问题的,因为列表的查询速度确实比链表快,链表要一个个问,这里用列表直接给索引就立马定位了,
单向链表负索引没有什么实际意义
这个列表看起来解决了给一个索引随机访问的问题,看似也给了迭代的方式。但是我们用链表的目的就是要与列表有区别,想在里面增删数据,,查询虽然有,但不是主要目的
所以这么修改,搞的链表不像链表,列表不像列表,一旦加入增删,发现这么设计就是失败的,看是加在一起,实际谁的优势都没发出来,这种情况下只能查多改少,查询特别多,改的特别少(直接用列表岂不更好)
所以经典的链表实现,就是纯粹的链表实现,没有人把字典,set,列表挪进去,进去之后就是四不像,链表不是链表,字典不是字典
每一个数据结构有自己的特点,混用只能在某一部分增强,为了保证列表,set,字典的一致性,就要增删的时候大家一起增删,会发现写的时候越来越麻烦,对于链表没有优化
这上面就删除,注释掉
一种是纯粹的链表实现,还有一个是本来想辅助的时候借用的列表
看似很好其实是有一些问题的,这样单向链表是完成了
双向链表
双向链表每一个节点指向前后两个地方
现在节点需要加一个,指向前一个节点
这样就解决打印的问题
关键是链表如何修改,第一个元素进来,头尾都是第一个元素,所以这两句不用动
加了元素,要提示上一个元素指向哪个,node元素的上一个,当前的节点是当尾巴的,当前尾巴的节点是准备要做尾巴的前一个,当前尾巴的下一个,就是node,这样相互手拉手
当前节点的左边,前一个修正了,下一个不用修正,因为是none,缺省值,当前节点的前一个不用管,当前尾巴的下一个要管,要添加 一个新的进来
self.tail=node 当前尾部要修正一下
双向迭代,加个reverse
如果不反转就是下一个next,反转就是上一个,prev,直到上一个或者下一个是none,就到头了,while current
下面代码不动,试试看
稍微做下改动就可以了,但是要把其他功能搞定
pop尾部移除,remove,任意位置删除,insert中间加入
节点可以了
append也搞定了
迭代可以两头迭代
这样基本算达到要求了
先只接受正索引
然后进行迭代,enumerate,迭代元素然后配个对
查看是否超界,超界代表迭代完了
如果index相同就找到了那个位置上了,就代表这里肯定有个元素
如果是空的,if self.head is none 说明现在没有元素,全空
如果遍历完都没找到你插入的位置,else,说明要么是你索引超界了,要么是正好超界了,是在尾巴上
for循环如果没有元素是可以直接不迭代的,
进入else,append
吵了,就要在队尾,追加一个元素,插入简单,但是要把关系,也要更新
这是把当前节点的上一个下一个搞定了
**A和 B中间加入c,c要指向B,B要指向C,A是原来B的前一个,C的上一个是A,就可以用prev **
假设你的插入点在0的位置,在头部插入,说明当前prev,也应该是none,头部插入不能说明下一个也是none,没有关系
空的是靠self.append(value)解决的
原来AB链接,加入C,就需要都重新链接
第一种和第二种的情况做的情况不一样
怎么知道是不是首部
第一种是prev is none
i==0,0是因为上面enumerate还是局部变量
上面用哪个都可以,这两个条件是等价的
node= Node(value)获得一个待加入结点对象
如果是第一个元素,index==0
node.next =current
新添入的节点左右就解决完了
self.head,就是头要改
c代表current,前一个是A(prev),后面的元素是next,加入一个 node,前面如果本身是none,就不用进行修改了,写一个修正成current,current的前一个修正尾node,current的下一个就不用管
在插入的时候有三个参与方,插入的算中间,中间的后一个是谁,
如果是在头部加入修正头,尾部加入修正尾
要是尾部加入,append(value)这步就做了,轮不到 下面了
中间加入试试,就需要把三个点的关系调整下
中间插入,修改的点,有4个需要考虑
现在这么看好像有重复的
就可以拿出来,里面的注释掉
上面是尾部追加,下面是非尾部追加,分为,头部,和中间加入,在头部追加的时候需要修正头,
这样insert方法基本搞定
insert(0,0)0 的位置加个0
在0 的位置再加个abc
在中间加一个
这就是insert,下面写pop,尾部移除
就是self.tail
如果return none,区分不出来是真正没有还是真正的none
拿到tail,里面一定封装一个数据,通过node.item拿出数据
如果pop尾部移除,那么前一个需要作为新的尾巴
但是如果把尾巴拿掉,前一个是none,说明里面只有一个元素,需要清空
self.head=none
self.tail=none
最后return item
还有其他的,假设2个元素,移除尾巴,最后剩一个
代表前一个元素的下一个原来有东西,现在要改成none, prev.next=none
前一个元素就要改成tail尾巴了。self.tail=prev
这就是pop
试试pop,现在end就不见了
pop完,空了,就没打印了
抛出empty,就没用了
pop先不用了
pop就完成了,下一步的remove相对来讲比较难
如果负索引就报错,
self.head is none代表里面什么都没有
else:
超出界了就直接删除最后一个即可
或者这么写
抛出异常,raise indexerror
break,说明移除的就是当前current
走到这一步就找到了要移除的节点
然后看看各种情况
要盯着index,不然不知道移除谁
一般移除头想,只需要修改头即可,移除尾只需要修改尾即可
4种情况:
如果正好是1个,移除,头尾都需要修正,prev is none and next is none
elif(代表前面测试过,一定不是前面判断过的)
prev is none 不是一个元素的链表,从头部移除
elif
next is none 不是一个元素的链表,从尾部移除
else:
不是一个元素,且从中间移除
如果正好是1个,移除,头尾都需要修正,
self.head=none
self.tail=none
移除头部,就需要改头部
self.head=current 头修正下
current.prev=none 当前节点做老大,原来有A,B,把原来老大A去掉,B作为新的老大,原来B指向A的就不见了,B就作为current了,下面一个就爱谁谁,不变了,就动头,当前
修改尾巴
当前节点移除 self.tail=prev,(如果是开头)
下一个就要做开头了,netx.prev=none
尾部移除,
current丢弃,self.tail=prev
前一个的下一个就是none了,prev.next=none
中间就不用修正头和尾
修改前后两个手拉手一起
prev.next=next
next.prev=prev
这就是写的双向链表
使用list是为了查询起来速度更好
增加列表,只能为查多,删除少的链表增速
看似兼顾优势,其实没什么
正反迭代如何思考,iter开头就是为了写生成器