012 python数据结构与算法:链表

链表

为什么需要链表

顺序表的构建需要预先知道数据大小来申请连续的存储空间,而在进行扩充时有需要进行数据的搬迁,所以使用起来并不是很灵活。链表结构可以充分的利用计算机内存空间,实现灵活的内存动态管理。

链表的定义

链表(Linked list)是一种常见的基础数据结构,是一种线性表,但是不像顺序表一样连续存储数据,而是在每一个节点(数据存储单元)里 存放下一个节点的位置信息(地址)。
在这里插入图片描述
和顺序表相比,顺序表存储空间必须连续,但如果有动态存储的需求时,顺序表就不太适用。
在顺序表中元素外置就是用一种联系来链接数据和地址。
在这里插入图片描述
如图所示,存储数据的三个内存空间并不是连续的内存空间,它们之间通过链相互链接。像这样,节点和节点之间通过链来链接起来的表叫做链表。
链表和顺序表统称为线性表,都是一维空间的线性结构。
链表中的每个节点由两部分组成:存储数据的区域和存储地址的区域。链接区存的是下一个节点的地址。
在这里插入图片描述

单向链表

单向链表也叫做单链表,是链表中最简单的一种形式。它的每个即诶单包含两个域,一个信息域(元素域)和一个链接域。这个链接指向链表中的下一个节点,而最后一个节点的链接域指向一个空值。
在这里插入图片描述

  • 表元素域elem用来存放具体的数据
  • 链接域next用力啊存放下一个节点的位置(python中的标识)
  • 变量p指向链表的头节点(首节点)的位置,从p出发能找到表中任意节点。

想要实现单链表的结构,必须先找到第一个节点的位置。图中,尾部位置的符号,表示这是最后一个节点,指向空。第一个节点叫做头节点,最后一个节点叫做尾节点。

节点实现

在python中,一切皆对象。即节点对象都有两个区域,因此需要先定义一个节点类。

class SingLinkNode(object):#节点有两个区域:数据区和链接区
    """单链表的节点"""
    def __init__(self,elem):
        #_item中存数据元素
        self.elem=elem
        #_next是下一个节点的标识
        self.next=None

python中没有地址的表示方式,所以我们需要给python进行扩充:
示例:在python中交换两个变量如何交换?
在这里插入图片描述
这里首先要看等式的右边,b,a分别等于20,10(是因为a,b变量分别指向存入10,20的内存空间),接着看等式的左边,让a,b分别改变指向的内存空间,以达到交换两个变量的目的。

由此可知这里的等号不是赋值,而是改变a,b的地址导向。python的变量名保存的是地址。a指向的是引用对象的地址,=就是产生一个引用的链接。所以也可以说变量的本质就是引用和链接。
同理,变量名中还可以指向对象方法,调用类方法。(因为python中一切都是对象都可以调用)

单链表的操作

  • is_empty()链表是否为空
  • length()链表长度
  • travel()遍历整个链表
  • add(item)链表头部添加元素
  • append(item链表尾部添加元素
  • insert(pos,item)指定位置添加元素
  • remove(item)删除节点(这里指的是指定的元素节点)
  • search(item)查找即诶单是逗存在

使用单链表的方法前需要先构造类,对象和函数,才可以调用。

#单链表

class SingLinkNode(object):#节点有两个区域:数据区和链接区
    """单链表的节点"""
    def __init__(self,elem):
        #_item中存数据元素
        self.elem=elem
        #_next是下一个节点的标识
        self.next=None


#node=Node(100)
class SingleLinkList(object):
    """单链表"""
    #def __init__(self):#外部不需要知道这个属性,所以需要进行私有
        #self._head=None#一开始头节点指向空
    def __init__(self,none=None):
        # 对象属性
        self.__head = none#设置默认的参数,使用户传入的链表可以是空链表也可以是含参链表
    def is_empty(self):
        """链表是否为空"""
    def length(self):
        """链表长度"""
    def travel(self):
        """遍历整个链表"""
    def add(self):
        """链表头部添加元素"""

注意在链表中需要有一个指向第一个节点的标识。

具体方法的实现

1.首先要创建一个新的单链表:

sll=SingleLinkList()

在这里插入图片描述
这是新创建的一个单链表中为空,所以sll单链表的头指针指向为空。
2.接着构造一个节点:

node=Node(100)

插入一个带数据为 100的节点
在这里插入图片描述
3.将节点挂到链表上,实际上就是让head指向node(第一个节点)
在这里插入图片描述
4.就可以通过构造函数去实现单链表中的各种操作方法
判断链表是否为空

 def is_empty(self):
        """链表是否为空"""
        return self.__head==None

当为空的时候,直接返回True
求链表长度(一次遍历)
遍历的时候就需要有一个辅助的东西从头到尾去计——叫做游标或者指针。
在这里插入图片描述
遍历的第一步就是需要有一个辅助的cur来指向当前的位置
那到底选择哪一个语句来作为循环条件,和起始值有关。
游标cur从第一个节点开始遍历(指向head时,count为0),当遍历到count=3时,已经满足cur.next=None,则不会进入循环体,则计数比实际会少1,因此在这时,应当选择cur!=none这条判断语句。
在这里插入图片描述

    def travel(self):
        """遍历整个链表"""
        cur = self.__head
        while cur != None:
            print(cur.elem, end=" ")
            cur = cur.next
        print(" ")
  def length(self):
        """链表求长度"""
        cur=self.__head#cur游标,用来移动遍历节点
        count=0#count记录数量
        while cur!=None:
            count+=1
            cur=cur.next#移动游标
        return count

这时需要去考虑存在链表为空的特殊情况:选择cur.next==None这个判断条件。

尾部插入:
先遍历链表,遍历到最后一个极点后,把心节点挂上去,就是尾插法。
但要注意的是,在append(item)方法中,传入的item不是一个节点,而是一个具体的数据元素。
在这里插入图片描述

    def append(self,item):
        """链表尾部插入元素"""
        node=SingLinkNode(item)#创建一个节点对象
        cur=self.__head#遍历节点的游标
        while cur.next!=None:
            cur=cur.next
        cur.next=node#在遍历到最尾节点时,将尾节点的指针指向新的节点

但是以上代码没有包含特殊情况,所以特殊情况需要特殊处理。就是当链表是空链表的情况:

 def append(self,item):
        """链表尾部插入元素"""
        node=SingLinkNode(item)#创建一个节点对象
        if self.is_empty(node):#判断链表是否为空,为空的话,则头节点指向空
            self.__head=node
        else:
            cur=self.__head#遍历节点的游标
            while cur.next!=None:
                cur=cur.next
            cur.next=node#在遍历到最尾节点时,将尾节点的指针指向新的节点

头部插入:
在这里插入图片描述
这里需要注意的是修改指针的先后顺序,为了保证不丢失后面的节点,我们需要先将待插入节点的next区域指向原头节点再将__head的next区域指向待插入节点。这样才能顺利完成头部插入。

    def add(self,item):
        """链表头部添加元素"""
        node=SingLinkNode(item)
        node.next=self.__head#要注意这里的self.head不是指head这个节点,而是指haed指向的原头节点
        self.__head=node

在指定位置插入元素:
在这里插入图片描述

    def insert(self,pos,item):
        """在指定位置插入元素"""
        pre=self.__head
        count=0#需要有一个计数的,这样才可以找到指定位置
        while count<(pos-1):#pos是从0开始的,pre是指定位置的前一个,所以需要-1
            count+=1
            pre=pre.next
        #当循环结束时,pre指向pos-1的位置
        node=SingLinkNode(item)
        node.next=pre.next#先将待插入节点的next域指向指定位置的下一个节点,保证节点插入不丢失后面节点
        pre.next=node

函数的形参pos指的是元素的下标,为了符合数据结构类型,所以pos也是从0开始索引。pre是指指定位置的前一位。
pre=self.__head理解这句话,首先看等式的右边,通过self.__head找到对应的元素,所以在上述代码中,pre指向的应该是第一个节点。
现在考虑关于pos的特殊情况:如果用户传过来的pos<0,则认为是头插法,pos传入的数据比链表本身长度值更大,则表示是尾插法。
因此可以更家完善这个方法:

    def insert(self,pos,item):
        """在指定位置插入元素"""
        if pos<=0:
            self.add(item)
        elif pos>(self.length()-1):
            self.append(item)
        else:
            pre=self.__head
            count=0#需要有一个计数的,这样才可以找到指定位置
            while count<(pos-1):#pos是从0开始的,pre是指定位置的前一个,所以需要-1
                count+=1
                pre=pre.next
            #当循环结束时,pre指向pos-1的位置
            node=SingLinkNode(item)
            node.next=pre.next#先将待插入节点的next域指向指定位置的下一个节点,保证节点插入不丢失后面节点
            pre.next=node

单链表节点查找:
查找的本质就是判断一个数据是否在链表中。

    def search(self,item):
        """查找节点是否存在"""
        cur=self.__head#从头开始遍历
        while cur!=None:
            if cur.elem==item:
                return True
            else:
                cur=cur.next
        return False

同样需要考虑一开始就是空链表的特殊情况,如果一开始为空的链表,则cur一开始就为None,因此不进入while循环,直接return False。
单链表的删除:
注意这里的删除不是按照具体的位置去删除,而是删除具体的数据。因为要找到具体的数据,所以是需要游标辅助遍历。
在这里插入图片描述
删除需要两个辅助游标,一个pre(指向前一个节点),一个cur(指向找到的那个节点)。那是否可以用一个游标去完成删除操作呢?答案是可以的,只需要一个pre游标,也可以时下删除指定数据操作。
在这里插入图片描述
那么在使用两个游标的情况下,如歌初始化两个游标:pre和cur始终相差一个节点的距离,pre在cur之前。因为一开始cur指向第一个节点,在cur之前没有节点,所以pre一开始是pre=None。在进行游标移动时:第一步是让pre=cur,第二步是让cur=cur.next

    def remove(self,item):
        """删除节点"""
        cur=self.__head#从头开始遍历
        pre=None#pre和cur始终相差一个节点的距离
        while cur!=None:
            if cur.elem==item:
                pre.next=cur.next
            else:
                pre=cur
                cur=cur.next

考虑两端的极限情况:链表为空或要删除的节点恰好是首节点的情况
一开始是空链表,则在执行方法时,不进入循环,对空链表不执行任何操作,上述代码满足要求。
那删除的节点恰好是头节点的情况:

 def remove(self,item):
        """删除节点"""
        cur=self.__head#从头开始遍历
        pre=None#pre和cur始终相差一个节点的距离
        while cur != None:
            if cur.elem==item:#首先判断一下第一个节点是不是指定数据节点
            # #头节点是指定节点
                if cur==self.__head:#cur指向的是头节点
                    self.__head=cur.next#直接删除
                else:#指向的不是头节点
                    pre.next=cur.next
                break
            else:
                pre=cur
                cur=cur.next

特殊情况:
1.当在这个链表中,只有头节点这一个节点,且需要指定删除头节点。
2.当删除的这个指定节点恰好也是最尾节点。
都可以通过以上代码解决。
测试以上方法:

#单链表

class SingLinkNode(object):#节点有两个区域:数据区和链接区
    """单链表的节点"""
    def __init__(self,elem):
        #_item中存数据元素
        self.elem=elem
        #_next是下一个节点的标识
        self.next=None


#node=Node(100)
class SingleLinkList(object):
    """单链表"""
    #def __init__(self):#外部不需要知道这个属性,所以需要进行私有
        #self._head=None#一开始头节点指向空
    def __init__(self,none=None):
        # 对象属性
        self.__head = none#设置默认的参数,使用户传入的链表可以是空链表也可以是含参链表
    def is_empty(self):
        """链表是否为空"""
        return self.__head==None
    def length(self):
        """链表长度"""
        cur=self.__head#cur游标,用来移动遍历节点
        count=0#count记录数量
        while cur!=None:
            count+=1
            cur=cur.next#移动游标
        return count
    def travel(self):
        """遍历整个链表"""
        cur = self.__head
        while cur != None:
            print(cur.elem, end=" ")
            cur = cur.next
        print(" ")

    def add(self,item):
        """链表头部添加元素"""
        node=SingLinkNode(item)
        node.next=self.__head#要注意这里的self.head不是指head这个节点,而是指haed指向的原头节点
        self.__head=node

    def append(self,item):
        """链表尾部插入元素"""
        node=SingLinkNode(item)#创建一个节点对象
        if self.is_empty():
            self.__head=node
        else:
            cur=self.__head#遍历节点的游标
            while cur.next!=None:
                cur=cur.next
            cur.next=node#在遍历到最尾节点时,将尾节点的指针指向新的节点
    def insert(self,pos,item):
        """在指定位置插入元素"""
        if pos<=0:
            self.add(item)
        elif pos>(self.length()-1):
            self.append(item)
        else:
            pre=self.__head
            count=0#需要有一个计数的,这样才可以找到指定位置
            while count<(pos-1):#pos是从0开始的,pre是指定位置的前一个,所以需要-1
                count+=1
                pre=pre.next
            #当循环结束时,pre指向pos-1的位置
            node=SingLinkNode(item)
            node.next=pre.next#先将待插入节点的next域指向指定位置的下一个节点,保证节点插入不丢失后面节点
            pre.next=node
    def search(self,item):
        """查找节点是否存在"""
        cur=self.__head#从头开始遍历
        while cur!=None:
            if cur.elem==item:
                return True
            else:
                cur=cur.next
        return False

    def remove(self,item):
        """删除节点"""
        cur=self.__head#从头开始遍历
        pre=None#pre和cur始终相差一个节点的距离
        while cur != None:
            if cur.elem==item:#首先判断一下第一个节点是不是指定数据节点
            # #头节点是指定节点
                if cur==self.__head:#cur指向的是头节点
                    self.__head=cur.next#直接删除
                else:#指向的不是头节点
                    pre.next=cur.next
                break
            else:
                pre=cur
                cur=cur.next

if __name__=="__main__":#一个python文件通常有两种使用方法,第一是作为脚本直接执行,第二是 import 到其他的 python 脚本中被调用(模块重用)执行。因此if __name__ == 'main': 的作用就是控制这两种情况执行代码的过程,在if __name__ == 'main': 下的代码只有在第一种情况下(即文件作为脚本直接执行)才会被执行,而 import 到其他脚本中是不会被执行的。
    ll=SingleLinkList()
    print(ll.is_empty())
    print(ll.length())

    ll.append(1)#1
    print(ll.is_empty())
    print(ll.length())

    ll.append(2)#1 2
    ll.add(8)#8 1 2
    ll.append(3)
    ll.append(4)
    ll.append(5)
    ll.append(6)#8 1 2 3 4 5 6
    ll.travel()
    ll.insert(-1,9)#9 8 1 2 3 4 5 6
    ll.travel()
    ll.insert(3,100)#9 8 1 100 2 3 4 5 6
    ll.travel()
    ll.insert(10,200)#9 8 1 100 23456 200
    ll.travel()

    ll.remove(100)
    ll.travel()
    ll.remove(9)
    ll.travel()
    ll.remove(200)
    ll.travel()

运行结果:
在这里插入图片描述

单链表与顺序表的对比

链表失去了顺序表随机读取的优点,同时链表由于增加了节点的指针域,空间开销比较大,但对于存储空间的使用要相对灵活。
链表域顺序表的各种操作时间复杂度如下:
在这里插入图片描述
需要注意的是,虽然时间复杂度都是O(n),但是链表和顺序表在插入和删除时进行的是完全不同的操作。链表存的主要耗时操作是遍历查找(链表只记录头节点地址,需要通过遍历来查找指定节点),删除和插入操作本身的时间复杂度是O(1)。顺序表查找很快,主要耗时操作是数据的搬迁。因为除了目标元素在尾部的特殊情况外,顺便表进行插入和删除时,需要对操作点之后的所有元素进行前后移位操作,只能通过拷贝和覆盖的方法进行。
链表和顺序表优缺点:

  • 链表存储数据时,内存可以不是连续的空间,可以通过链表来串联分散不连续的存储空间,但是由于链表节点包含两个部分(数据域和指针域),因此链表会消耗比顺序表更大的空间。
  • 链表充分利用计算机中零碎的内存空间,存储更灵活,能够动态的进行存储,不需要像顺序表一样事先预估申请内存空间。
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值