链表的提出
链表与顺序表的区别:
顺序表的特点:要求存储空间必须连续的,在扩充数据的时候,一旦存储空间不够的情况下,就需要涉及到动态的改变。换言之,顺序表的构建需要预先知道数据大小来申请连续的存储空间,而在进行扩充时又需要进行数据的搬迁,所以使用起来并不是很灵活。
数据区。
链表:在扩充数据的时候,原有的数据完全不用变,多一个就增加一个。换言之,链表结构可以充分利用计算机内存空间,实现灵活的内存动态管理。
如图:
链表的定义
链表(Linked list)是一种常见的基础数据结构,是一种线性表,但是不像顺序表一样连续存储数据,而是在每一个节点(数据存储单元)里存放下一个节点的位置信息(即地址)。
顺序表和链表都是线性表,也就是说,无论是连续存储的还是用线串联起来的,最终表现的是一维空间的线。后面将拓展到二维,就变成了树,三维就是图了!
单向链表单向链表也叫单链表,是链表中最简单的一种形式,它的每个节点包含两个域,一个信息域(元素域)和一个链接域。这个链接指向链表中的下一个节点,而最后一个节点的链接域则指向一个空值。
第一个节点为头节点;最后一个节点为尾节点。
编译器的概念。
要产生一个数据结构,要把关于这个数据结构以及它的操作放到一起形成一个整体,这就是面向对象的类的概念。一个保存问题和定义出支持哪些操作。
引一个实例:交换两个变量
a = 10 b = 20 a, b = b, a
a = 10 ,就是先产生一个变量,然后把10放入。也就是说,遇到a=10,首先要向操作系统申请一个存储单元,把10保存,这个时候,a是变量名,在其他语言中,a就是这个存储单元的别名,但在python中,a不代表10,它是10有一块存储空间保存下来了,紧接着,10要放到a里面去,a的这块空间去指向10,a保存的是10的地址。b=20,先找一块内存,把b=20中的20先存起来,接着产生一个标号b,这个变量,再找一块内存把b与这个内存空间绑定一起,也就是说,这个b代表的是一个内存空间,只不过这个空间存放的不是数值20,而是存储的20这个存储空间的地址上了。最后,a, b = b, a,要从逗号右边开始,先看b,就要找顺着b指向的位置存的是谁,也就是20;逗号后的a同理;等号右边有了之后,看等号前面。赋值并不是真正的赋值,而是把a和b引用的地址指向改变一下。
变量指向的位置存储的是什么。也就是说变量a是一个存储单元,它存储的是存放10的地址。
a=10 ,a是一个整型。
python中一切皆对象。函数f也是一个对象。为了保存这个函数,得有一个区域把函数保存起来,这个就是定义的函数f。
在python中,随意写得变量a,可以等于任何东西,可以是一个数字、字符串,或者一个函数,或者一个类。也就是说,这个变量a,只是一个名字,或者说是一个引用,它维护的只是一块真实的地址,这个地址指向的东西不同,那么这个a代表的东西也不一样。这与其他语言的区别。比如说,在C语言中,我们构建一个变量,我们必须指明这个变量到底是什么类型,比如,假如是一个整形,必须前面加上int(int a = 10),我这个a才真正代表了是内存空间,我找了一块内存,存的是10,这个10的空间别名就是a,一旦有了这样的位置后,你a这个位置只能存整数,你i可以改变这个位置的值,也就是可以将10改为其他整数,比如5,但不能把这个a指向一个函数或者类,因为这个a这个位置保存的只能是整形,是我一开始构建的时候就定下来了。在python当中,对一个变量的声明也是一个声明,不去使用它的类型,也就是说,它也能声明的原因是,让a=10的时候,a真正代表的是一个内存,它内存当中保存的是一块地址,这个地址指向什么,就意味着a的变量是谁了,a=10,实际上,a这个地方并没有保存10。
a等于谁,就意味着这个区域指向了那个节点。
单链表的操作:
is_empty() 链表是否为空
length() 链表长度
travel() 遍历整个链表
add(item) 链表头部添加元素
append(item) 链表尾部添加元素
insert(pos, item) 指定位置添加元素
remove(item) 删除节点
search(item) 查找节点是否存在
元组也可以实现节点。但为了实现通用性,用类来实现。
single_link_list.py
## 节点类 class Node(object): """节点""" def __init__(self,elem): # 构造函数 # _item存放数据元素 self.elem = elem # _next是下一个节点的标识 self.next = None # node = Node(100) ## 链表类 class SingleLinkList(object): """单链表""" def __init__(self, node = None): # 头节点 self.__head = node # head对自己内部使用,对外不公布,即私用的属性,前面加__ def is_empty(self): """判断链表是否为空""" return self.__head == None # 也就是说头节点为空 def length(self): """链表长度""" # cur游标,用来移动遍历节点, cur初始时指向头节点 cur = self.__head # count 记录数量 count = 0 # 注意这里是0 # 尾节点指向None,当未到达尾部时 while cur != None: count += 1 # 将cur后移一个节点 cur = cur.next return count def travel(self): """遍历整个链表""" # cur游标,用来移动遍历节点 cur = self.__head # 开始时,指向头节点 while cur != None: print(cur.elem, end="") # 打印这个元素的值,end=" "换行 cur = cur.next print("") # 换行 def add(self, item): """链表头部添加元素,头插法""" # 先创建一个保存item值的节点 node = Node(item) # 将新节点的链接域next指向头节点,即_head指向的位置 node.next = self.__head # 将链表的头_head指向新节点 self._head = node def append(self, item): # item不是节点,是一个具体的数据 """链表尾部添加元素,尾插法""" node = Node(item) # 先判断链表是否为空,若是空链表,则将_head指向新节点 if self.is_empty(): # 或者写成 self._head = None self._head = node # 头节点没有next # 若不为空,则找到尾部,将尾节点的next指向新节点 else: cur = self._head while cur.next != None: cur = cur.next cur.next = node def insert(self, pos, item): """指定位置添加元素 :param pos从0开始 """ # 若指定位置pos为第一个元素之前,则执行头部插入 if pos <= 0: self.add(item) # 若指定位置超过链表尾部,则执行尾部插入 elif pos > (self.length()-1): # pos >= self.length() self.append(item) # 找到指定位置 else: node = Node(item) # 创建新的节点 count = 0 # pre用来指向指定位置pos的前一个位置pos-1,初始从头节点开始移动到指定位置 pre = self._head while count < (pos-1): # count <= pos count += 1 pre = pre.next # 当循环退出之后,pre指向pos-1位置 # 先将新节点node的next指向插入位置的节点 node.next = pre.next # 将插入位置的前一个节点的next指向新节点 pre.next = node def remove(self,item): """删除节点""" cur = self.__head pre = None while cur != None: # 找到了指定元素 if cur.elem == item: # 先判断次结点是否是头节点,即如果第一个就是删除的节点 if cur == self.__head: # not pre: # 将头指针指向头节点的后一个节点 self.__head = cur.next else: # 将删除位置前一个节点的next指向删除位置的后一个节点 pre.next = cur.next break else: # 继续按链表后移节点 pre = cur cur = cur.next def search(self, item): """链表查找节点是否存在,并返回True或者False""" cur = self.__head # 空链表的头节点就是None while cur != None: if cur.elem == item: return True cur = cur.next return False # 对象方法,不是类方法,原因:在取得时候,构造一个对象等于类,去初始化。 if __name__ == "__main__": ll = = SingleLinkList() # 构造了类的实例对象 print(ll.is_empty()) print "length:",ll.length() ll.add(1) ll.add(2) ll.append(3) ll.insert(2, 4) ll.insert(4, 5) ll.insert(0, 6) print "length:",ll.length() ll.travel() print ll.search(3) print ll.search(7) ll.remove(1) print "length:",ll.length() ll.travel()
图解上诉函数的流程思路:
需要一个游标(cur,当前的位置)来标记当前位置,
尾部添加节点
实现部分代码是def append()函数
头部添加节点
add函数
先让新节点的next指向原链表的头节点,然后再将原有链表的头节点指向新的链表。
若是原来的链表是空链表,同样适用。
指定位置添加元素
insert函数
查找
search函数
def search(self,item): # 空链表的头节点就是None """链表查找节点是否存在,并返回True或者False""" cur = self._head # 开始指向头节点 while cur != None: # 非空 if cur.elem == item: return True cur = cur.next return False
删除
remove函数
或者一个游标实现方式:
def remove(self,item): """删除节点""" cur = self._head pre = None while cur != None: # 找到了指定元素 if cur.item == item: # 如果第一个就是删除的节点 if not pre: # 将头指针指向头节点的后一个节点 self._head = cur.next else: # 将删除位置前一个节点的next指向删除位置的后一个节点 pre.next = cur.next break else: # 继续按链表后移节点 pre = cur cur = cur.next
后继结点:当前结点的下面一个结点。
链表失去了顺序表随机读取的优点,同时链表由于增加了结点的指针域,空间开销比较大,但对存储空间的使用要相对灵活。
链表与顺序表的各种操作复杂度如下所示:
注意虽然表面看起来复杂度都是 O(n),但是链表和顺序表在插入和删除时进行的是完全不同的操作。链表的主要耗时操作是遍历查找,删除和插入操作本身的复杂度是O(1)。顺序表查找很快,主要耗时的操作是拷贝覆盖。因为除了目标元素在尾部的特殊情况,顺序表进行插入和删除时需要对操作点之后的所有元素进行前后移位操作,只能通过拷贝和覆盖的方法进行。
链表不能一次性定位到指定的位置。链表只记录了头结点,要找到其他结点,必须从头结点开始查找。
顺序表:存储数据的时候,尾部插入或者删除,可以以O(1)的方式,一次性定位,缺点是,它的存储空间必须是连续的,一旦动态的改变,整个存储区域都要改变,保存的数据大的话,可能就没有连续大的内存空间存储。
链表:存储的结点对应到内存中,内存可以是分散的,假如要存一个巨大的数据,就不需要一个连续的内存空间。存数据的时候,比顺序表多占内存,但内存又可以分散的。
在中间插入/删除,链表的 O(n)花费在了遍历上了,而顺序表的 O(n)花费在了搬迁数据上了,也就是它们的操作是不一样的。
单向循环链表是在单链表的基础上进行的一个改进。单链表的一个变形是单向循环链表,链表中最后一个节点的next域不再为None,而是指向链表的头节点。
## 节点类 class Node(object): """节点""" def __init__(self,elem): # 构造函数 # elem存放数据元素 self.elem = elem # _next是下一个节点的标识 self.next = None # node = Node(100) ## 链表类 class SinCycLinkedlist(object): """单向循环链表""" def __init__(self, node = None): # 头节点 self.__head = node # head对自己内部使用,对外不公布,即私用的属性,前面加__ if node: # 指向链表自身 node.next = node def is_empty(self): """判断链表是否为空""" return self.__head == None # 也就是说头节点为空 def length(self): """链表长度""" if self.is_empty(): return 0 # cur游标,用来移动遍历节点, cur初始时指向头节点 cur = self.__head # count 记录数量 count = 1 # 注意这里是0 # 尾节点指向None,当未到达尾部时 while cur.next != self.__head: # count += 1 # 将cur后移一个节点 cur = cur.next return count def travel(self): """遍历整个链表""" if self.is_empty(): return cur = self.__head # 开始时,指向头节点 while cur.next != self.__head: print(cur.elem, end="") # 打印这个元素的值,end=" "换行 cur = cur.next # 退出循环,cur指向尾结点,但尾结点的元素未打印 print(cur.elem) print("") # 换行 # 先找尾结点, def add(self, item): """链表头部添加元素,头插法""" # 先创建一个保存item值的节点 node = Node(item) # 空链表,就意味着只有头结点,头结点没有next if self.is_empty(): self.__head = node node.next = node # node指向自身 else: cur = self.__head while cur.next != self.__head: cur = cur.next # 退出循环,cur指向尾结点 node.next = self.__head # 将链表的头_head指向新节点 self._head = node cur.next = node # cur.next = self.__head def append(self, item): # item不是节点,是一个具体的数据 """链表尾部添加元素,尾插法""" node = Node(item) # 先判断链表是否为空,若是空链表,则将_head指向新节点 if self.is_empty(): # 或者写成 self._head = None self.__head = node # 头节点没有next node.next = node # 若不为空,则找到尾部,将尾节点的next指向新节点 else: cur = self.__head while cur.next != self.__head: cur = cur.next ## 下面两句话可以换位置,不影响 node.next = self.__head # 将node指向头节点_head node.next = node def insert(self, pos, item): """指定位置添加元素 :param pos从0开始 """ # 若指定位置pos为第一个元素之前,则执行头部插入 if pos <= 0: self.add(item) # 若指定位置超过链表尾部,则执行尾部插入 elif pos > (self.length()-1): # pos >= self.length() self.append(item) # 找到指定位置 else: node = Node(item) # 创建新的节点 count = 0 # pre用来指向指定位置pos的前一个位置pos-1,初始从头节点开始移动到指定位置 pre = self._head while count < (pos-1): # count <= pos count += 1 pre = pre.next # 当循环退出之后,pre指向pos-1位置 # 先将新节点node的next指向插入位置的节点 node.next = pre.next # 将插入位置的前一个节点的next指向新节点 pre.next = node def remove(self,item): """删除节点""" if self.is_empty(): return cur = self.__head pre = None while cur.next != self.__head: # 找到了指定元素 if cur.elem == item: # 先判断此结点是否是头节点,即如果第一个就是删除的节点 if cur == self.__head: # 头节点的情况 # 找尾结点 rear = self.__head while rear.next != self.__head: rear = rear.next self.__head = cur.next rear.next = self.__head else: # 中间节点 # 将删除位置前一个节点的next指向删除位置的后一个节点 pre.next = cur.next return # 单向链表不同之处 else: # 继续按链表后移节点 pre = cur cur = cur.next # 退出循环,cur指向尾结点 if cur.elem == item: if cur == self.__head: # pre == None # 链表只有一个节点 self.__head = None # self.__head = pre else: pre.next = self.__head # pre.next = cur.next def search(self, item): """链表查找节点是否存在,并返回True或者False""" if self.is_empty(): return False cur = self.__head # 空链表的头节点就是None while cur.next != self.__head: if cur.elem == item: return True else: cur = cur.next # 退出循环,cur指向尾结点 if cur.elem == item return True return False if __name__ == "__main__": ll = SinCycLinkedlist() # 构造了类的实例对象 print(ll.is_empty()) print "length:",ll.length() ll.add(1) ll.add(2) ll.append(3) ll.insert(2, 4) ll.insert(4, 5) ll.insert(0, 6) print "length:",ll.length() ll.travel() print ll.search(3) print ll.search(7) ll.remove(1) print "length:",ll.length() ll.travel()
链表可不可以包含
双向链表一种更复杂的链表是“双向链表”或“双面链表”。每个节点有两个链接:一个指向前一个节点,当此节点为第一个节点时,指向空值;而另一个指向下一个节点,当此节点为最后一个节点时,指向空值。
后继节点:当前节点的下一个结点;
前驱结点:当前结点的上一个结点。
操作
is_empty() 链表是否为空
length() 链表长度
travel() 遍历链表
add(item) 链表头部添加
append(item) 链表尾部添加
insert(pos, item) 指定位置添加
remove(item) 删除节点
search(item) 查找节点是否存在
double_link_list.py
## from single_link_list.py import SingleLinkList ## 结点 class Node(object): """双向链表节点""" def __init__(self, item): self.elem = item self.next = None self.prev = None ## 链表 ## 面向对象的继承 class DoubleLinkList(object): """双向链表""" def __init__(self): self._head = None # 指向头结点,空为None # 和单链表一样 def is_empty(self): """判断链表是否为空""" return self.__head is None # 也就是说头节点为空 # 和单链表一样 def length(self): """链表长度""" # cur游标,用来移动遍历节点, cur初始时指向头节点 cur = self.__head # count 记录数量 count = 0 # 注意这里是0 # 尾节点指向None,当未到达尾部时 while cur != None: count += 1 # 将cur后移一个节点 cur = cur.next return count # 和单链表一样 def travel(self): """遍历整个链表""" # cur游标,用来移动遍历节点 cur = self.__head # 开始时,指向头节点 while cur != None: print(cur.elem, end="") # 打印这个元素的值,end=" "换行 cur = cur.next print("") # 换行 # 涉及到前结点 def add(self, item): """链表头部添加元素,头插法""" # 先创建一个保存item值的节点 node = Node(item) # 将新节点的链接域next指向头节点,即_head指向的位置 node.next = self.__head # 将链表的头_head指向新节点 self.__head = node node.next.prev = node # def append(self, item): # item不是节点,是一个具体的数据 """链表尾部添加元素,尾插法""" node = Node(item) # 先判断链表是否为空,若是空链表,则将_head指向新节点 if self.is_empty(): # 或者写成 self._head = None self._head = node # 头节点没有next # 若不为空,则找到尾部,将尾节点的next指向新节点 else: # 移动到链表尾部 cur = self.__head while cur.next != None: cur = cur.next # 将尾节点cur的next指向node cur.next = node # 将node的prev指向cur node.prev = cur def insert(self, pos, item): """指定位置添加元素 :param pos从0开始 """ # 若指定位置pos为第一个元素之前,则执行头部插入 if pos <= 0: self.add(item) # 若指定位置超过链表尾部,则执行尾部插入 elif pos > (self.length()-1): # pos >= self.length() self.append(item) # 找到指定位置 else: cur = self.__head count = 0 while count < pos: count += 1 pre = pre.next # 当循环退出之后,cur指向pos位置 node = Node(item) # 创建新的节点 # 先将新节点node的next指向插入位置的节点 node.next = cur node.prev = cur.prev cur.prev.next = node cur.prev = node def remove(self,item): """删除节点""" if self.is_empty(): return cur = self.__head while cur != None: # 找到了指定元素 if cur.elem == item: # 如果首节点的元素即是要删除的元素 # 头结点 if cur == self.__head: self.__head = cur.next if cur.next: # 判断链表是否只有一个结点 cur.next.prev = None else: # 将cur的前一个节点的next指向cur的后一个节点 cur.prev.next = cur.next if cur.next: # 尾部结点 cur.next.prev = cur.prev break else: # 继续按链表后移节点 cur = cur.next def search(self, item): """链表查找节点是否存在,并返回True或者False""" cur = self.__head # 空链表的头节点就是None while cur != None: if cur.elem == item: return True cur = cur.next return False # 对象方法,不是类方法,原因:在取得时候,构造一个对象等于类,去初始化。 if __name__ == "__main__": dll = = DoubleLinkList() # 构造了类的实例对象 print(ll.is_empty()) print("length:",ll.length()) dll.add(1) dll.add(2) dll.append(3) dll.insert(2, 4) dll.insert(4, 5) dll.insert(0, 6) print("length:",ll.length()) dll.travel() print(dll.search(3)) print(dll.search(7)) dll.remove(1) print("length:",dll.length()) dll.travel()
头部插入
add函数
方法1
方法2
append,尾插法
insert函数
方法1
方法2
search函数
字符串、图、字典