zoukankan      html  css  js  c++  java
  • 线性表

    阅读目录

    • 一、线性表的概念和表抽象数据类型
    • 二、顺序表的实现
    • 三、链接表
    • 四、链表的变形和操作
    • 五、课后部分编程练习(初学时写的,仅供参考)

    一、线性表的概念和表抽象数据类型

    1、表的概念和性质

    线性表示某类元素的一个集合,记录着元素之间的一种顺序关系。

    理解表的下标,空表,表的长度,顺序关系,首元素,尾元素,前驱元素和后继元素等概念。

    2、表抽象数据类型

    从实现者角度考虑两个问题:

    •  如何把该结构内部的数据组织好。
    •  如何提供一套有用并且必要的操作。

    线性表的操作

    •  创建操作。考虑提供初始元素的序列问题。
    •  解析操作。判断是否为空,表的长度,取得表内特定数据等。
    •  变动操作。添加删除特定元素等。
    •  连表操作。两表组合得到新表等。
    •  遍历操作。对每个元素进行操作等。

    表抽象数据类型

    3、线性表的实现

    研究数据结构的实现问题,主要考虑两个方面的问题:

    •  计算机内存的特点。以及如何保存元素的关系。(隐式还是显式)
    •  各种重要操作的效率。

    根据上面两个问题,得出两种基本的实现模型:

    •  将元素顺序放在一大块的连续存储区内,这样实现的表称之为顺序表。
    •  将元素放在通过链接构造起来的一系列存储块中,这样的实现称为链接表。

    二、顺序表的实现

    顺序表的实现思路很简单,表中的元素顺序放在一片足够大的连续内存中。首元素存入存储区的开始位置,其余元素依次存储。元素之间的关系通过元素在存储区里的物理位置表示(隐式表示元素间的关系)。

    1、基本实现方式

    • 表里要保存的元素类型相同,因此每个表元素所需的存储量相同,可以根据公式Loc(ei) = Loc(e0) + c * i很快求出元素ei的地址,从而访问到它的元素。
    • 表中的元素大小不一。将实际元素另行存储,在顺序表里的各单元保存的是对相应元素的引用信息。这样,通过上面公式计算出引用信息的位置,在通过引用信息访问到元素本身。这样的顺序表也被称为对实际数据的索引。

    线性表的一个重要的性质是可以添加删除元素,那么就会带来一个问题,我们在建立一个表时,需要给它分配多大的内存?

    一个合理的办法是分配完已有的元素之外,再留一些空位以备添加操作使用。

    上图实例是一个顺序表的完整信息。包括容量大小,元素个数,以及各个元素内容。

    2、顺序表基本操作的实现

    创建和访问操作

    • 创建空表,创建空表时,需要分配一块元素存储,记录表的容量并将元素计数值设置为0。
    • 简单判断操作。表空或者表满都是O(1)。
    • 访问给定下标i的元素。O(1)。计算地址,访问元素。
    • 遍历操作。O(n)。
    • 查找给定元素d的(第一次出现的)位置,这种操作称为检索或者查找。在没有其他信息的情况下,只能通过用d与表中元素逐个比较的方式实现检索,称为线性检索。O(n)。
    • 查找给定元素d在位置k之后的第一次出现的位置。与上面操作类似。O(n)。

    最后几个操作都需要检查表元素的内容,属于基于内容的检索。数据存储和检索是一切计算和信息处理的基础

    不修改表结构的操作只有两种模式,一种是直接访问,一种是基于一个整型变量,按照下标循环并检查和处理(也就是变量操作)。

    变动操作:加入元素

    尾端加入新数据项。把新数据项存入表中的第一个空位,即下标num的位置。如果这个时候表满了,操作就失败。显然这个操作是O(1)。

    新数据存入元素存储区的第i个单元。这是一般情况。

    首先需要下标是否合法,要把新数据存入这里,又不能简单抛弃原有的数据,就必须把该项数据移走。

    移走方式有两种:

    • 不需要保持原有的序列,那么只需要把原来处于i位置的元素移到最后面,然后把新元素放到i元素的位置。这种操作能在O(1)时间完成。
    • 如果要保持原有的序列,就需要不断把i位置之后的元素逐一后移。这种操作最坏和平均复杂度都是O(n)。python采用这种方法。

    变动操作:删除元素

    尾端删除数据。将元素计数值num-1,就相当把表尾的元素删除了。O(1)。

    删除位置id数据。这个跟加入跟加入操作类似,也是分两种情况。

    • 如果没有保序要求,就追吧num-1位置的元素拷贝过去,覆盖掉原来的元素。
    • 如果有保序要求,就需要删除原来的元素之后,逐一上移其余元素。显然非保序定位删除操作的时间复杂度是O(1)。而保序定位删除的复杂度是O(n),因为其中可能会移动一系列元素。

    基于条件的删除

    这种删除操作并不是给定被删元素的位置,而是给出需要删除的数据项本身。或是一个满足删除的条件。这个显然跟检索有关,需要先找到元素,再删除它。

    顺序表及其操作的性质

    顺序表的各种访问操作,如果其执行中不需要扫描表内容的全部或者一部分,其时间复杂度都是O(1),需要扫描表内容操作时间复杂度都是O(n)。

    顺序表的优点:

    •  可以O(1)时间的按位置访问元素。
    •  元素在表里的存储紧凑,只需要O(1)空间存放少量辅助信息。

    缺点:

    • 如果表很大,即需要很大片的连续内存空间。
    • 内存一旦确定大小,就不能变化,导致内存利用不合理。
    • 在执行一般加入和删除操作的时候,通常需要移动元素,效率低。
    • 在建表的初始就需要考虑存储区大小。 

    3、顺序表的结构

    一个顺序表的完整信息包括两部分,一部分是表中的元素集合,另一部分是为实现正确操作而需要记录的信息(也就是表中的个数和大小等信息)。

    两种基本实现方式

    一体式实现,有关信息跟表元素放在一起。

    • 优点:比较紧凑,有利于管理。
    • 缺点:不同的表对象大小不一。还有就是创建之后元素的存储区就固定了。

    分离式实现,在表对象里只保存与整个表有关的信息,实际元素存放在另外一个独立的元素存储区对象里,通过链接与基本表对象关联。

    • 优点:表对象大小统一,不同对象关联不同大小的元素存储区。
    • 缺点:表的创建和管理,必须考虑多个独立对象。

    替换元素存储区

    分离式实现的最大优点是可以在标识不变的情况下,为表对象更换一块元素存储区。

    操作流程:

    1. 另外申请一块更大的元素存储区。
    2. 把表中已有的元素复制到新存储区。
    3. 用新的元素存储区替代原来的元素存储区(改变表对象的元素区链接)
    4. 实际加入新的元素。

    人们把利用采用这种技术实现的表称之为动态顺序表。

    后端插入和存储区扩充

    把一个动态顺序表从0逐渐扩大到n,如果是前端添加,那么整个就是O(n2)复杂度。
    如果是从后端添加,一次操作是O(1)的复杂度,不过这里要考虑一个问题,就是元素的替换问题。当整个表满的时候,就会申请一个新的内存复制已有的元素,这个操作是O(m)复杂度,m是当时表中元素的个数。

    那么就有一个问题,如果安排初始表中元素的数量,以及它的扩展策略?

    很明显,如果一次扩展的多,会造成内存空间的浪费,而如果扩展的不多,那么就会频繁地进行复制替换操作,造成性能下降。

    下面用两种不同的扩展策略来说明一下这个问题。

    • 线性增长策略。即每次扩展固定数目的元素。假设每次替换增加10个元素。那么,从0到1000总的复制次数是10+20+....+990 = 49500 = 1/20*n的平方,这样即使是单次操作的尾端添加O(1)也会让整个过程变成O(n的平方)。原因就是频繁的替换元素存储区,而且随着元素变多,每次替换复制的元素也越来越多。因此,应该考虑一种策略是随着元素数量的增加,替换存储区的频率不断的降低。
    • 加倍操作。每次扩展原元素数据的一倍。假设表元素从0到1024,那么表增长过程中复制元素的次数是1+2+4+8+....+512=1023=log2(下标)1024 -1。这样在整个表增长的过程中,元素的赋值次数也就是O(n)。每次插入操作的平均复杂度是O(1)。

    当然,后一个策略也有缺点,比如当元素很多的时候,一次扩展的空间就很大,这样如果用不完,就会造成空间浪费。解决方法,可以两种策略一起使用,另外如果对于高要求的程序,需要增加一个设定容量的操作,当下面的操作需要高要求的时候,就事前把表的容量修改到一个足够大的值,保证在关键计算的时候不会出现替换操作。

    4、python中list

    list的基本实现技术

    python中的list就是一种元素个数可变的线性表,并且添加删除元素后维持原来的顺序。

    • 基于下标的高效位置访问和更新。O(1)。
    • 允许任意添加元素,不会出现表满的情况,在添加的过程中对象的标识不变。(分离式结构的动态顺序表)。
    • 表中的元素保存在一块连续的存储区内。
    • 使用加倍策略。初始的时候是8,然后一次替换加4倍。当到达50000的时候,变成加倍策略。

    一些主要操作的性质

    • len()。O(1)
    • 元素访问和赋值。O(1)
    • 一般位置添加,删除,切片,extend,pop()非尾端删除,都是O(n)操作。

    python的一个问题是没有提供list检查容量的操作,也没有设置容量的操作,所有关于容量的操作都由python解释器自动完成。

    • 优点:降低编程人员的负担。
    • 缺点:限制了表的使用方式。

    其他几个操作

    • clear()。O(1)。可以另外分配一个空表,然后更改存储区,等待解释器回收内存块。也可以将表的计数值设置为0,但是这种并没有释放内存。
    • reverse()。看下面代码,很容易得出他是O(n)操作。
    • sort()。O(nlogn)。具体参考后面章节的排序。
    def reverse(self):
        elems = self.elems
        i, j = 0, len(elems)-1
        while i < j:
            elems[i], elems[j] = elems[j], elems[i]
            i, j = i+1, j-1
    list的反转

    三、链接表

    1、实现链接表的基本思路

    实现线性表的另外一种方式就是链接表,它用链接关系显示表示元素之间的顺序关系。这种表也称为链表。

    基本思路:

    * 把表中元素分别存储在一批独立的存储块(表的结点)中。
    * 保证从组成表结构的任一结点可找到与其对应的下一个结点。
    * 在前面一个结点里面显示记录下一个结点的链接。

    这样只要找到了第一个结点,就能找到所有的结点。

    2、实现单链表的操作分析

    单链表的结点是一个二元组,它的表元素域elem保存着作为表元素的数据项(或者数据项的关联信息),链接域next保存同一个表里下一个结点的标识。

    一个单链表不仅要有结点,还需要一个表头指针,这样就可以通过这个指针找到所有表中的节点。这个表头指针最开始指向的是表头节点。

    • 一个单链表由一些具体的表结点构成。
    • 每个结点是一个对象,有自己的标识。
    • 结点之间通过结点链接建立起单向的顺序。

    为了表示一个链接的结束,只需要给表的最后结点的链接域设置一个None值。

    class LNode:
        def __init__(self, elem, next_=None):
            self.elem = elem
            self.next = next_  #为了不与python的next重名
    链表的结点类

    基本链表操作

    • 创建空链表。只需把表头指针设置为空链接,也就是None就可以了。
    • 删除链接表。在C里,需要通过明确的操作释放一个个节点所用的存储。在python里,只需要把表指针指向None,python解释器就会回收表结点中不用的存储。
    • 判断表是否为空。表指针是否为None。
    • 判断表是否满。一般链表不会满,除非存储空间用完。

    加入操作

    表首端插入

    1. 创建一个新结点,并存入数据。
    2. 将新结点的引用域(next)设置为原表头。
    3. 将表头指针指向新的结点。

    可以看出这只需要几个赋值操作,因此复杂度是O(1)。

    一般情况的插入

    1.  创建一个新结点q,并存入数据。
    2.  找到要插入结点的前一个结点pre。这时pre的下一个结点是pre.next
    3. 先把新结点q的引用域设置为p以前的下一个结点也即是pre.next。再把pre的引用域(next)设置为新结点q。

    这个操作主要是第2步需要找到要插入结点的前一个结点,需要遍历整个链表。因此整个复杂度是O(n)。

    删除元素

    删除表首元素

    让表头指针指向表的第二个元素就可以了。python内存管理器会自动回收表头元素。也就是,self.head = self.head.next。

    一般情况的元素删除

    一般情况的删除跟一般情况的添加一样,需要找到相关元素。删除的时候,先找到要删除的前一个元素p,然后把它的引用(next)设置为要删除元素的后一个元素。也就是pre.next = pre.next.next

    扫描、定位和遍历

    在上面不论是一般删除还是一般添加,都需要找到要操作元素的前一个结点。由于单链表只有一个方向,因此,从表头指针开始,做一个循环,就能找到整个链表中的所有结点。这个过程称为链表扫描。

    p = head  # 扫描指针
    while p is not None and 其他条件:
        对p中的数据做操作
        p = p.next
    扫描操作伪代码
    p = head
    while p is not None and i > 0:
        i -= 1
        p = p.next
    按下标定位
    p = head
    while p is not None:
        if elemt == p.elem:
            return 
        p = p.next
    按照元素定位

    链表操作的复杂度

    • 创建空表O(1)
    • 删除表O(1)
    • 判断空表O(1)
    • 加入元素,首端加入O(1),一般加入O(n)。
    • 删除表元素,删除表头O(1),一般删除O(n)。

    表的长度

    看表的设计,如果设计表时,有表长度len的参数,那么删除,添加元素必须维持这个参数。这时表的长度就是O(1)。只需获得属性值就可以。

    如果没有这个参数,每次就长度,就需要遍历链表。O(n)的复杂度。

    p = head
    i = 0
    while p is not None:
        p = p.next
        i += 1 
    表长度操作

    3、单链表的实现

    class LinkedListUnderFlow(ValueError):
        '''
        自定义异常类,为了更快定位是链表中的异常,从而进行处理。
        '''
        pass
    定义链表的异常类
    class LNode:
        '''
        链表中的节点类
        '''
        def __init__(self, elem, next_=None):
            self.elem = elem
            self.next = next_
    链表的结点类
    class LList:
        '''
        链表的实现
        '''
        def __init__(self):
            slef.head = None   # 表头指针
        
        def empty(self):
            '''
            判断表是否为空表
            '''
            return self.head is None
        
        def prepend(self, elem):
            '''
            在表首端添加元素
            '''
            self.head = LNode(elem, self.head)
        
        def pop(self):
            '''
            删除表头结点并返回删除的元素
            '''
            if self.empty():
                raise LinkedListUnderFlow('in pop')
            e = self.head.elem
            self.head = self.head.next
            return e
        
        def append(self, elem):
            '''
            在表尾添加元素
            '''
            if self.empty():    # 如果表是空的,必须设置表头指针指向新元素
                self.head = LNode(elem)
                return 
            p = self.head   # 扫描指针
            while p.next is not None: # 找到表中的最后一个元素p。
                p = p.next
            p.next = LNode(elem)
        
        def pop_last(self):
            '''
            删除表中的最后一个元素,并返回其所删除的元素
            '''
            if self.empty():
                raise LinkedListUnderFlow('in pop_last')
            if self.head.next is None:  # 表中只有一个元素
                e = self.head.elem
                self.head = None
            else:
                p = self.head
                while p.next.next is not None:  # 找到要删除元素的前一个元素
                    p = p.next
                e = p.next.elem  
                p.next = None
            return e
        
        def find(self, pred):
            '''
            满足条件pred的第一个元素
            '''
            p = self.head
            while p is not None
                if pred(p.elem):
                    return p.elem
                p = p.next
        
        def print_all(self):
            '''
            打印链表中所有的元素,以,分隔开
            '''
            p = self.head
            while p is not None:
                print(p.elem, end='')
                if p.next is not None:
                    print(',', end='')
                p = p.next
            print('')  # 只是为了输出一个换行符
            
        def for_each(self, proc):
            '''
            对每个元素做一个指定的操作函数
            '''
            p = self.head
            while p is not None:
                proc(p.elem)
                p = p.next
        
        def elements(self):
            '''
            遍历这个链表,使用迭代器
            '''
            p = self.head
            while p is not None:
                yiel p.elem
                p = p.next
        
        def filter(self, pred):
            '''
            满足条件pred的所有元素
            '''
            p = self.head
            while p is not None:
                if pred(p.elem):
                    yiel p.elem
                p = p.next
    单链表类

    四、链表的变形和操作

    1、单链表的简单变形

    前面的单链表有一个缺点,如果要从尾端加入元素的话,必须要先找到最后一个元素,这样复杂度就成为了O(n)。

    可以通过给表增加一个尾结点的引用域,也就是尾指针,这样就可以在O(1)的时间找到尾元素,从而达到O(1)的复杂度。

    新的表除了尾部添加功能,跟尾部删除功能,以及与尾指针有关的操作不一样外,其他的操作跟单链表一样,因此,可以选择继承这个单链表。

    class LList1(LList):
        def __init__(self):
            super().__init__()
            self.rear = None
        
        def prepend(self, elem):
            '''
            首端添加,注意,当为空表的时候,必须设置尾指针。
            '''
            if self.empty():
                self.head = LNode(elem)
                self.rear = self.head
                return
            self.head = LNode(elem, self.head)
        
        def append(self, elem):
            '''
            尾端添加,可以达到O(1)的复杂度,因为不用循环表去找最后一个结点了。通过尾指针就可以直接找到。这个设计跟前面求表长的设计一样,都是通过一个维持一个属性值的正确性,来减少遍历的操作
            '''
            if self.empty():
                self.head = LNode(elem)
                self.rear = self.head
                return 
            self.rear.next = LNode(elem) #将新结点放入尾结点的next引用域,使之与链表串起来
            self.rear = self.rear.next  #将尾指针指向链表
        
        def pop_last(self):
            '''
            尾端删除,还是O(n)的复杂度,因为尾指针只能找到最后一个结点,而尾端删除需要找到最后一个结点的前一个结点。因此还需要循环一遍表,唯一与前面不同的地方是,删除的时候需要维护尾指针。
            '''
            if self.empty():
                raise LinkedListUnderFlow('in pop_last')
            if self.head.next is None:
                e = self.head.elem
                self.head = self.rear = None  # 这一步也可以直接写成self.head = None,因为判断表是否为空是用首指针来判断的。只要手指针为空就确定这个表没有元素了。
            else:
                p = self.head
                while p.next.next is not None:
                    p = p.next
                e = p.next.elem
                p.next = None
                slef.rear = p
            return e
            
    带尾指针的链表变形

    首端删除跟前面单链表是一样的,只要首指针指向None就说明表空,因此在首端删除的时候,并不需要维护尾指针。

    表设计的内在一致性

    就是在一个类内部,如果其他条件不变,设计原则尽量保持统一。

    def prepend(self, elem):
            '''
            这是从首端添加的另一个版本,虽然代码肯定正确,而且代码量还少,但是它有一个不好的地方,就是判断表空的时候使用的尾指针,这就违背了表设计的内置一致性的原则。不值得推荐。因为在这个类的其他方法中,检查表空都是用的首指针。
            '''
            self.head = LNode(elem, self.head)
            if self.rear is None:  # 判断是空表
                self.rear = self.head
    不一致的例子

    2、循环单链表

    单链表的另外一个变形是循环单链表,其中最后一个结点的next域不是指向None,而是指向表的第一个结点。这样就只需要一个表尾指针就可以了。

    循环链表与普通链表的不同之处就是在于表循环结束的控制条件。其他,就是尾指针的维护,大体思路跟链表是一样的。

    class LCList:
        def __init__(self):
            self.rear = None
        
        def empty(self):
            return self.rear is None
            
        def prepend(self, elem):
            '''
            前端插入元素
            '''
            if self.empty():
                self.rear = LNode(elem)
                self.rear.next = self.rear  # 建立一个结点的自循环
            else:
                self.rear.next = LNode(elem, self.rear.next)
        
        def append(self, elem):
            '''
            后端插入元素
            '''
            self.prepend(elem)
            self.rear = self.rear.next
        
        def pop(self):
            '''
            前端删除元素
            '''
            if self.empty():
                raise LinkedListUnderFlow('in LCList pop')
            if self.rear.next is sel.rear:
                e = self.rear.elem
                self.rear = None
            else:
                e = self.rear.next.elem  # 前端的元素
                self.rear.next = self.rear.next.next
            return e
        
        def print_all(self):
            '''
            打印整个循环链表
            '''
            if self.empty():
                return 
            p = self.rear.next
            while p is not self.rear:
                print(p.elem, end=',')
                p = p.next
            print(p.elem)
    循环单链表的实现

    3、双链表

    单链表只能支持一个方向的扫描和逐步操作,因此它不能达到O(1)的尾部删除时间复杂度。如果希望两端插入和删除操作都能高效完成,就需要两个方向都能扫描,这样要在结点中增加一个向前的引用域。然后,通过尾指针向前一步,就很容易找到这个要删除尾结点的前一个元素。

    class DLNode(LNode):
        def __init__(self, elem, prev=None, next_=None)
            super().__init__(elem, next_)
            self.prev = prev
    
    class DLList(LList1):
        '''
        继承带有尾指针的单链表
        '''
        def __init__(self):
            super().__init__()
        
        def prepend(self, elem):
            '''
            首端添加
            需要维护结点的引用域prev,因此需要改写
            '''
            if self.empty():
                self.head = DLNode(elem)
                self.rear = self.head
            else:
                self.head.prev = DLNode(elem, next_=self.head)  #将原来首结点的prev域设置为新结点。并将新结点的next域设置为原来的首结点
                self.head = self.head.prev  #将首指针指向新结点
        
        def append(self, elem):
            '''
            尾端添加
            '''
            if self.empty():
                self.head = DLNode(elem)
                self.rear = self.head
            else:
                self.rear.next = DLNode(elem, prev=self.rear)
                self.rear = self.rear.next
        
        def pop(self):
            '''
            首端删除
            '''
            if self.empty():
                raise LinkedListUnderFlow('in DLList pop')
            e = self.head.elem
            if self.head.next is None:
                self.head = None
            else:
                self.head.next.prev = None  # 将第二个元素的prev引用域改为None
                self.head = self.head.next  # 将首指针指向第二个元素
                
            return e
        
        def pop_last(self):
            '''
            这个是O(1)操作,是经过了改良之后有着好性能的方法
            '''
            if self.empty():
                raise LinkedListUnderFlow('in DLList pop_last')
            e = self.rear.elem  # 最后一个结点元素
            if self.head.next is None:
                self.head = None
            else:
                self.rear.prev.next = None # 将最后一个结点的前一个元素的next引用域设置为None。
                self.rear = self.rear.prev
            return e    
    双链表的实现

    循环双链表

    双链表也可以定义为循环链表。如下图

    4、两个链表操作

    链表反转

    先回忆list的reverse,用两个下标,通过逐对交换元素位置,直到两个下标碰头,来完成反转操作。

    同样的思路也可以用在双链表上。

    def reverse(self):
        '''
        双链表反转的方法,采用搬动元素的思路。
        '''
        i = self.head
        j = self.rear
        while i.prev is not j:  # 当它的i超过j的时候终止循环,完成反转
            i.elem, j.elem = j.elem, i.elem
            i = i.next
            j = j.prev
    搬动元素实现反转双链表

    上面的反转双链表的思路跟反转list的思路是一样的,都是搬动结点的元素。这种思路应用在单链表上也可以完成,但是复杂度O(n2)比较高,因为没有从后向前的方向指针。

    因此,这里考虑另外一种反转链表的思路,那就是更改链表的引用域。通过改变结点的链接顺序来改变表元素的顺序。

    基本思路:从一个表的首端不断取下结点,将其加入另一个表的首端,这样就完成了一个反转过程。

    def reverse(self):
        '''
        单链表反转的方法,采用更改引用域的思路。O(n)的复杂度
        '''
        if self.empty():
            return
        p = None
        while self.head is not None:
            q = slef.head  # 不断拿到首结点
            self.head = q.next  # 将首指针往后移动
            q.next = p   # 更改它的next域,使之指向下一个拿到的首结点
            p = q
        self.head = p
    采用更改链表引用域反转单链表

    链表排序

    先对list的插入排序了解一下,后面有详细介绍。也可以直接看第9章排序的插入排序部分。

    def insert_sort(lst):
        for i in range(1, len(lst)):
            tmp = lst[i]  # 找到无序区的第一个元素,把它插入有序区合适的地方
            j = i
            while j > 0 and lst[j-1] > tmp: #如果超出下标或者找到要插入的位置,循环结束
                lst[j] = lst[j-1] # 将比较大的元素逐一后移
                j -= 1
            lst[j] = tmp # 将元素插入找的那个位置
        return lst
    list的插入排序

    使用插入排序的思路和移动链表元素的思路对链表进行排序。与顺序表的插入排序不同是,它寻找要插入的位置不同。顺序表是从右往左移动寻找,而单链表由于只有一个方向,从左往右移动,因此它是从左往右移动去寻找。当找到位置之后,再用一个临时变量保序原位置的elem。以便后来循环插入使用。

    def sort1(self):
        '''
        使用插入排序移动元素的思路对单链表进行排序
        '''
        if slef.empty():
            return
        p = self.head.next
        while p is not None:
            e = p.elem # 无序区的第一个元素
            q = self.head 
            while q is not p and q.elem <= e: # 从前往后找到要插入元素的节点位置
                q = q.next
            while q is not p:  # 找到位置,倒腾元素逐一后移
                tmp = q.elem  # 将q位置的元素先存起来
                q.elem = e  # 然后将e位置元素放到q位置
                e = tmp    # 将e的元素更换为以前q位置的元素,以备下次循环替换
                q = q.next
            q.elem = e  # 将最后一个元素回填
            p = p.next
    采用搬用元素的思想实现链表的插入排序

    第二种单链表插入排序的实现方法,调整链表的引用域来完成。

    def sort2(self):
        '''
        使用插入排序移动元素的思路对单链表进行排序
        '''
        p = self.head
        if p is None or p.next is None:
            return
        rem = p.next  # 取得无序区的第一个元素
        p.next = None    # 将首元素的next引用域设置为None,因为反转之后它就变成最后一个元素了。
        while rem is not None:
            p = self.head
            q = None
            while p is not None and p.elem <= rem.elem:
                q = p
                p = p.next
            if q is None:
                self.head = rem
            else:
                q.next = rem
            q = rem
            rem = rem.next
            p.next = p
    采用更改引用域的思想实现链表插入排序

     五、课后部分编程练习

    # 给链表添加本章开始定义的线性抽象数据类型中没有的操作。
    class LList:
        def __len__(self):
            p = self._head
            e = 0
            while p is not None:
                e += 1
                p = p.next
            return e
                
        def insert(self, elem, i):
            if i < 0 or i > len(self):
                raise LinkedListUnderFlow
            elif i == 0:
                self.prepend(elem)
            else:
                p = self._head
                while p is not None and i > 1:
                    i -= 1
                    p = p.next
                p.next = LNode(elem, p.next)
    
        def delt(self, i):
            if i < 0 or i >= len(self) or self._head is None:
                raise LinkedListUnderFlow
            elif i == 0:
                self.pop()
            else:
                p = self._head
                while p is not None and i > 1:
                    i -= 1
                    p = p.next
                p.next = p.next.next
    
        def search(self, elem):
            p = self._head
            e = 0
            while p is not None:
                if p.elem == elem:
                    return e
                e += 1
                p = p.next
            return -1
    
                
    课后第一题
    # 请为Llist类增加定位插入和删除操作。
    # 答案:见上面第一题
    课后第二题
    """
    给Llist增加一个元素计数值域num,并修改类中操作,维护这个计数值。
    另外定义有一个求表中元素个数的len函数。
    请比较这种实现和原来没有元素计数值域的实现,
    个说明其优缺点。
    """
    
    #答案:
    #这个实现很简单,就是在
    def __init__(self):
        self._head = None
        self._num = 0
    #初始化计数值为0。
    #然后在进行加入元素操作的时候,比如prepend,append,insert等方法中,令self._num += 1
    #在删除元素操作的时候,比如pop,pop_last,delt等方法中,令self._num -= 1
    #最后写一个len方法。
    def len(self):
        return self._num
    #这样做的好处是可以在O(1)时间内得到链表的个数,而原来是O(n)时间。
    #因此代码执行效率大大提高了。
    #但是它的缺点是要实时维护一个计数变量num。
    课后第三题
    """
    课后练习第四题
    请基于元素相等操作'=='定义一个单链表的相等比较函数。
    另请基于字典序的概念,为链接表定义大于、小于、大于等于和小于等于判断。
    """
    class LList:
    
        def __init__(self):
            self._head = None
     # ==
        def __eq__(self, other):
            if not isinstance(other, LList):
                raise TypeError
            p, q = self._head, other.head()
            while (p is not None) and (q is not None):
                if p.elem == q.elem:
                    p, q = p.next, q.next
                else:
                    return False
            if (p and q) is not None:
                return False
            return True
    
        # <
        def __lt__(self, other):
            if not isinstance(other, LList):
                raise TypeError
            p, q = self._head, other.head()
            while (p is not None) and (q is not None):
                if p.elem < q.elem:
                    return True
                elif p.elem == q.elem:
                    p, q = p.next, q.next
                else:
                    return False
            if (p is None) and (q is not None):
                return True
            return False
    
        # >
        def __gt__(self, other):
            if not isinstance(other, LList):
                raise TypeError
            p, q = self._head, other.head()
            while (p is not None) and (q is not None):
                if p.elem > q.elem:
                    return True
                elif p.elem == q.elem:
                    p, q = p.next, q.next
                else:
                    return False
            if (p is not None) and (q is None):
                return True
            return False
    
        # <=
        def __le__(self, other):
            if not isinstance(other, LList):
                raise TypeError
            p, q = self._head, other.head()
            while (p is not None) and (q is not None):
                if p.elem < q.elem:
                    return True
                elif p.elem == q.elem:
                    p, q = p.next, q.next
                else:
                    return False
            if (p is not None) and (q is None):
                return False
            return True
    
        # >=
        def __ge__(self, other):
            if not isinstance(other, LList):
                raise TypeError
            p, q = self._head, other.head()
            while (p is not None) and (q is not None):
                if p.elem > q.elem:
                    return True
                elif p.elem == q.elem:
                    p, q = p.next, q.next
                else:
                    return False
            if (p is None) and (q is not None):
                return False
            return True
    课后第四题
    """
    请为链接表定义一个方法,它基于顺序表参数构造一个链接表,
    另请定义一个函数,它从一个链接表构造出一个顺序表
    """
        def list_llist(self, mlist):
            for i in range(len(mlist)-1, -1, -1):
                self.prepend(mlist[i])
    
        def llist_list(self):
            return [i for i in self.elements()]
    课后第五题
    """
    请为单链表类增加一个反向遍历方法rev_visit(self, op),它能从后向前的顺序把操作op逐个作用于表元素。你定义的方法在整个遍历中访问点的次数与表长度n是什么关系?
    如果不是线性关系,请设法修改,使之达到线性关系。这里要求遍历方法的空间代价是O(1)
    提示:
    你可以考虑为了遍历而修改表的结构,只要能在遍历的最后将表的结构复原。
    """
            
        def rev_visit(self, op):
            self.reverse()
            self.for_each(op)
            self.reverse()
    课后第六题
    """
    请为单链表类定义下面几个元素删除方法,并保持其他元素的相对顺序
    1、del_minimal() 删除当时链表中的最小元素
    2、del_if(pred) 删除当前链表里所有满足谓词函数pred的元素
    3、del_duplicate()删除表中所有重复出现的元素。也就是说,表中任何元素的第一次出现保留不动,
    后续与之相等的元素都删除
    要求所有的操作,复杂度均为O(n)
    """
    
        def del_minimal(self):
            if self._head is None:
                raise LinkedListUnderFlow
            p = self._head
            min_num = p.elem
            while p.next is not None:
                if min_num > p.next.elem:
                    min_num = p.next.elem
                p = p.next
    
            p = self._head
            q = None
            while p is not None:
                if not q and p.elem == min_num:
                    self._head = self._head.next
                    q = self._head
                elif p.elem == min_num:
                    q.next = p.next
                    if p.next is None:
                        return
                else:
                    q = p
                p = p.next
    
        def del_if(self, pred):
            """"
            这种方法比较复杂,不能达到O(n)的复杂度要求
            """
            # p = self._head
            # index, i = [],0
            # while p is not None:
            #     if pred(p.elem):
            #         index.append(i)
            #     p = p.next
            #     i += 1
            #
            # print(index)
            # for j in range(len(index)-1,-1,-1):
            #     self.delt(index[j])
            # return index
            p = self._head
            q = None
            while p is not None:
                if not q and pred(p.elem):
                    self._head = self._head.next
                    q = self._head
                elif pred(p.elem):
                    q.next = p.next
                    if p.next is None:
                        return
                else:
                    q = p
                p = p.next
    
        def del_duplicate(self):
            p = self._head
            mlist = []
            q = None
            while p is not None:
                if p.elem not in mlist:
                    mlist.append(p.elem)
                    q = p
                else:
                    q.next = p.next
                    if p.next is None:
                        return
                p = p.next
    课后第七题
  • 相关阅读:
    【转】软链接和硬链接到底有啥作用和区别
    useradd命令详解
    【转】Linux下MySQL数据库安装及配置方法
    【转】MySQL的安装与配置——详细教程-window系统下
    mysql服务器常用命令
    【转】DDL/DML/DCL区别概述
    tmux终端工具的简单使用
    linux go环境安装和基本项目结构
    ClickHouse高可用集群的配置
    centos7下使用rpm包安装clickhouse
  • 原文地址:https://www.cnblogs.com/walle-zhao/p/11384606.html
Copyright © 2011-2022 走看看