4.单链表(node的核心知识,指针思想)---理解复杂数据结构的基础。

(一)回顾线性结构,对比单链表和线性结构

回顾线性结构:
线性结构的增、删,如果经常涉及到首部元素的添加删除,则会使得效率由O(1)来到O(n)。

而在末尾添加元素则偶尔会设计数组扩容问题,也会影响效率。

但是数组的内部遍历访问效率为O(1),因为底层连续存储和索引的设计。

对比单链表结构:

单链表访问、遍历的效率方面由于没有索引和连续存储的原理,不如线性结构。

单链表的强就强在,线性结构的劣势之处:
单链表设计了root(头)和整个链表的next(尾),二者使得单链表可以快速获取’首、尾’地址。
这使得单链表在首 | 尾 处插入、删除"操作时能快速精确定位内存地址,达到O(1)的效率。

(二)、单链表的具体分析

0.操作前找到前驱节点的重要性

注:单链表的插入、删除操作使用中,我们只注重两个对象,即前驱节点和新加入的节点,而不需要注重间接受到影响的第三者,操作过程不涉及第三者。

1.new_node对象是必然的:

在单链表的底层实现中:各个方法里都写了new_node=Node(value)
插入操作显然需要先实例化这么一个对象。

2.前驱对象,对单链表"插入、删除"操作的必要前提就是我们已知前驱对象的引用。

解释为什么用不到第三者

使用单链表时候,从来不会用到插入或删除位置的后继对象,
因为前驱对象.next这种表达方式再或者前驱对象.next.next
这两种写法,总有一种可以表达出间接影响到的对象。

因此,两个对象足矣,根本不需要第三者的出现,
只需要理解node并熟练使用node里面的next所表达的含义

1.单链表结构的构成,是如何设计的:

从整体上看:单链表是由一个个node利用next链接起来的一个不连续的表。

单链表自身设计结构来看(设计了"哨兵节点和尾指针")
单链表设计了属于LinkList自身的‘root和next’:

(1.)哨兵节点:单链表对象内部有root(哨兵节点,或叫’虚拟头节点’),用于简化链表头部位置的操作,它等价于一个只有next指针,而不设置value的node,只要熟练使用哨兵节点,通过root.next可以方便地实现头部的操作

(2.)链表自身的尾指针:含有一个属于LinkList而不属于Node的’next’,持续更新记录LinkList尾部最后一个节点,每次尾部的操作之后更新。

<1>Node的构成

1.整个链表由多个Node节点组成

2.Node里面有value用于存放值,有next用于存放下一个Node的地址。

各个Node以如下方式链接起来(并非是代码,只是生动描述这种链接关系)

node1:(value)next----  node2地址
node2:(value)next----  node3地址
node3:(value)next....  ...地址

<2>LinkList的构成

(1)哨兵节点root:
	1.哨兵节点:
LinkList对象的构造方法内部会设置一个哨兵节点root,
目的是简化位于链表位于头部的操作。
	
具体做法示例:
class LinkList():
	def __init__(self):
		self.root=Node()
        self.size=0
        self.next=None

哨兵节点的创建惯例:
	设计一个,哨兵节点root,通过在构造方法中,
	先将它赋值为一个空的实例对象,
	它里面不存放任何的value,
	只需要注重root哨兵节点的next对象的更改。
	
方便理解:我们可以把哨兵节点想象成一个类似value=0的空块,永远放在
LinkList的开头位置,这样画图分析也时候就很好理解)
如下图:这就是使用哨兵节点root的生动示例,
	可以看到,在"涉及头部的相关操作"时候,
	它十分有用,可以快速从链表中找到对应头部位置。

在这里插入图片描述

(2)next:记录整个链表的末尾节点
2.LinkList的next------整个链表的next:
	
具体做法示例:
class LinkList():
	def __init__(self):
		self.root=Node()
        self.size=0
        self.next=None
        
上面的代码解释:是对于next的初始化的步骤
	LinkList的next值,next在构造方法时候先置空,
	涉及链表末尾的记录将会持续更新。

2.单链表为什么能在"首尾"插入删除时,具有O(1)效率

单链表在首,尾,插入删除操作的效率是O(1),
这O(1)的效率发生在特定情景下:已知了‘前驱节点的引用’,
即可直接访问前驱节点。(已知内存的位置直接访问的手段永远是最快的操作)
那么我们对root和next的引用已知,所以首尾的操作来到了O(1)效率

单链表删除原理(下面讲的是通用的删除原理,不是’头、尾’特殊位置处)

单链表删除操作原理示例:
node1:(value)next---- node2地址
node2:(value)next---- node3地址这里改为node4地址
node3:(value)next… node4地址
node4:(value)next---- …地址
已知node2的地址,引用node2,把node2的next改为node4的地址,即删除了node3

单链表插入原理(也是通用原理,不是’头/尾’处)

单链表插入操作原理示例:
node1:(value)next---- node2地址
node2:(value)next---- node3地址这里改为node2.5地址
新增:node2.5(value)next----node3地址
node3:(value)next… node4地址
node4:(value)next---- …地址

这是插入的原理,但是我们往往不在中间插入。

原因:单链表不是连续存储也没有索引,只能从头开始遍历找到你所修改位置的前驱,
只有头插和尾插才是推荐的选择,因为我们单链表的设计
LinkList自带的root和next决定了头尾具有着高效率。
单链表插入存在逻辑顺序(原链表断裂)

第一次思考插入操作:
已知node2的地址,引用node2,把node2的next改为要插入的node的地址,然后将新插入的node的next也链接上原来剩余的部分。

思考:但是这里存在顺序问题,node3的地址怎么知道呢?回想到node3的地址在node2的next有记录,可是被先一步被更改了,应该调整一下第一次思考中的顺序

第二次思考时候:(解决第一次思考中的链表中途断裂问题。)

先把插入的node中的next链接上暂时未被更改的前驱节点中的next,再去更改前驱节点的next为插入的node的地址。

简单记忆为:
插入操作中,单链表中不要'先一步对"前驱节点的next做修改,先一步对其做修改会导致原链表断裂。

值得一提的是,如果你理解了此处的原链表断裂的事情,
那你每次进行插入、删除操作的时候,都会额外留意插入、删除其中的顺序问题

单链表的’头插’和’尾插’(最为推荐,效率最高且实现逻辑简单)

单链表的头插和尾插逻辑变得好编写

1.不存在单链表的断裂问题
2.在头插的插入逻辑中不必先一步判断链表此时是否为空状态

3.单链表的劣势:

设计到遍历链表则来到O(n)的效率

单链表的访问操作,需要根据单链表的结构中"从一个中找到下一个",
一个一个遍历的模式,使得链表的遍历访问来到O(n)的效率。

因此单链表不适合获取元素,
而更适合在已知前驱位置的情况下(如首、尾),插入新节点,或者删除旧节点。

(三)单链表涉及的ADT及其实现:

Node的ADT以及实现

# 从链表结构开始,很多的结构都要先实现一个Node

# Node的ADT及实现  

from abc import ABC,abstractmethod

class NodeADT(ABC):
    '''这个ADT类是对Node的基本构成进行练习'''

	@abstractmethod
    def __init__(self,value,next):
        '''使它用value就能实例化Node'''
        pass

    @abstractmethod
    def get_value(self):
        return
        
    @abstractmethod
    def set_value(self,value):
        pass
        
    @abstractmethod
    def get_next(self):
        return

    @abstractmethod
    def set_next(self,next):
        pass

class Node(NodeADT):

    '''实现Node'''
    def __init__(self,value=None,next=None):
        '''使之用value即可实现node的实例化'''
        self.value=value
        self.next=None
    
    def get_value(self):
        return self.value
        
    def set_value(self,value):
        self.value=value
        
    def get_next(self):
        return self.next

    def set_next(self,next):
        self.next=next


LinkList(单链表)的ADT以及实现

了解哨兵节点root的设计(虚拟头节点)

关于哨兵节点root的设计:

1.哨兵节点root的初始化:将root初始化为一个空Node()
class LinkList():
	def __init__(self):
		self.root=Node()
        self.size=0
        self.next=None

2.哨兵节点的灵活使用:root.next
def append_head(self,value):
        new_node=Node(value)
        self.next=self.root.next
        self.root.next=new_node

LinkList的ADT

# LinkList(单链表)的ADT及实现
class LinkListADT(ABC):
    '''单链表的ADT的构思'''
    @abstractmethod
    def __init__(self):
        '''需要先用Node实例化空对象作为链表,链表类的每一个方法内都要新建对应的Node然后用node对象进行相关操作'''
        pass

    @abstractmethod
    def append(self,value):   # append默认是加在链表尾部
        '''默认,即在尾插,插前是否为空,是空还要对LinkList的root和next进行更新'''
        pass

    @abstractmethod
    def append_head(self,value):
        '''在头插,如果插的时候是空还要对LinkList的root和next进行更新'''
        pass

    @abstractmethod
    def __iter__(self):
        '''从头遍历到尾,并输出链表的实际内容'''
        yield

    @abstractmethod
    def delete(self):
        '''在尾删'''
        pass

LinkList的具体实现:

# LinkList的实现
class LinkList(LinkListADT):
    '''单链表的LinkList的实现'''

    def __init__(self):
        self.root=Node()
        self.size=0
        self.next=None


    def append(self,value):             # append默认加在尾部
        '''在尾插,value是用于创建新node的'''
        new_node=Node(value)
        '''这里做一个if判断,末尾添加时候先判断是否整个单链表为空,非空时候时候,挂在next.next,因自占一处'''
        if not self.next:
            self.root.next=new_node   #先挂上(由于空,挂在头部)新节点
            self.next=new_node        # 再更新,链表尾部的指针。
        else:
            self.next.next=new_node   #先挂上(由于非空,挂在尾部)新节点
            self.next=new_node       #再更新,链表尾部的指针。
        
        self.size+=1    #这里顺便将链表的size +1
        '''
        你也可以改写成,把if else相同的两行移出来。
         if not self.next:
            self.root.next=new_node
        else:
            self.next.next=new_node
        self.next=new_node
        self.size+=1
        '''

    def append_head(self,value):
        '''头插,先插上,再去更新链表的指针'''
        new_node=Node(value)
        new_node.next=self.root.next
        self.root.next=new_node
        '''
        另外,如果已经理解了原链表断裂问题,那么你会理解上面这两行的顺序问题。
        '''
        if not self.next:
            self.next=new_node.next
        '''这里的if就是将链表自身的next值仍旧是None进行更新'''
    
    def __iter__(self):
        '''从头遍历到尾,引入一个current变量,并利用它输出链表的实际内容'''
        current=self.root.next
        # 先将current值赋值为头root.next
        while current is not self.next:
            #current此时获取到的root.next不与link7list的next指向同一个对象,那么就意味着单链表内不为空
            yield current.value
            # yield类似return,在专门将迭代的对象一个个输出时候使用。
            current=current.next 
            # current指向下一个值
        yield current.value
            #单链表为空时候也设置输出

    def delete(self,value):
        '''在尾删'''
        if not self.root.next:
            return "1.先判断链表为空"
        '''2.设置current变量用于遍历'''
        current=self.root.next
        # 在delete中同样需要先遍历找到对应位置

        
        while current.next and current.next.value != value:
                #有下个节点不为空,且下个节点不是要找的值时候,就一直遍历,如果找到就自动跳出while
             current = current.next
    
        if current.next.value==value:
              # 如果找到了要删除的节点
            current.next = current.next.next
              # 将它的next跳到下一个,即修改完成
            return "删除成功"

(四)什么是伪代码,如何利用伪代码

1.伪代码是什么:
伪代码是用类似自然语言的代码来体现一个小部分功能的逻辑,
它可以体现对于功能实现的基本逻辑,而不用真的实现它。

伪代码是解释复杂算法的好工具,可以让我们迅速理解一个算法的核心,
让程序员对于一个算法具备核心的理解,而不拘泥于语言,在团队中为其它成员讲解伪代码时候也很实用。

2.利用伪代码:

<1.>  用于练习分解、分析复杂问题的过程:
Procedure SolveComplexProblem()
    Step 1: 分析问题,明确目标
    Step 2: 分解问题,列出所有子问题
    Step 3: 逐个解决子问题
    Step 4: 合并子问题的解决方案,得到最终结果
EndProcedure

<2.>  用于抽象出一个算法实现细节的具体步骤,通过编写伪代码来简化和概括代码的逻辑,再逐渐思考细节如何实现:

Procedure AbstractAlgorithm()
    Step 1: 初始化
    Step 2: 执行核心逻辑
    Step 3: 处理边界情况
    Step 4: 返回结果
EndProcedure


<3.>  在阅读源码时候,可以将其转换成伪代码加深自己理解,或用伪代码作为算法的注释。
# 源码中的伪代码注释

def calculate_sum(numbers):
    # 初始化总和为0
    total_sum = 0
    # 遍历每个数字并累加
    for num in numbers:
        total_sum += num
    # 返回总和
    return total_sum

# 对应的伪代码
Procedure CalculateSum(numbers)
    Initialize total_sum to 0
    For each number in numbers do
        Add number to total_sum
    EndFor
    Return total_sum
EndProcedure


(五)、为什么学习单链表

单链表貌似只有首尾的插入删除具有效率优势,但存在一些遍历的缺陷。

后面会学习双链表和更复杂的树形结构之类的内容,
之所以学习单链表,是为了理解好后续的复杂数据结构打下坚实的基础,
单链表里面我们清楚地学习了Node的相关知识、指针、利用指针进行插入删除遍历等的操作。

单链表的自身使用不多,但是基于单链表,经常使用的各种数据结构:

《双链表\循环链表\栈和队列\图和树\哈希表》

以上这些经典的数据结构,都或多或少的使用到了单链表的知识,因此在这一节学好单链表是必要的。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值