算法通关村第一关——1.青铜挑战_小白也能学会的链表(python)

本文详细介绍了Python中的链表构造,包括单链表的概念、节点和头结点、虚拟结点的作用,以及如何在链表的首部、中部和尾部增加和删除元素。通过实例讲解了链表的创建、遍历、插入和删除方法,特别关注了处理特殊情况的技巧。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

问题思考:

1.理解Python是如何构造出链表的

2.链表增加元素,首部、中间和尾部分别会有什么问题,该如何处理?

3.链表删除元素,首部、中间和尾部分别会有什么问题,该如何处理?

1.单链表的概念

算法的基础的数据结构,任何数据结构的基础都是创建+增删改查,所有的链表算法题分解到最后,都是这几个操作。

1.1 链表的概念

首先看一下什么是链表?单向链表就像一个铁链一样,元素之间相互连接,包含多个结点,每个结点有一个指向后继元素的next指针。表中最后一个元素的next指向null。如下图:
在这里插入图片描述

1.2 思考一下

你是否理解了链表的含义了呢?思考下面两个图,是否都满足单链表的要求,为什么?
第一个图:
在这里插入图片描述
第二个图:
在这里插入图片描述
解析:
上面第一个图是满足单链表要求的,因为我们说链表要求环环相扣,核心是一个结点只能有一个后继,但不代表一个结点只能有一个被指向。第一个图中,c1被a2和b3同时指向,这是没关系的。这就好比法律提倡一夫一妻,你只能爱一个人,但是可以多个人爱你。
第二个图就不满足要求了,因为c1有两个后继a5和b4.
另外在做题的时候要注意比较的是值还是结点,有时候两个结点的值相等,但并不是同一个结点,例如下图中,有两个结点的值都是1,但并不是同一个结点。
在这里插入图片描述

2.链表的相关概念

2.1 节点和头结点

在链表中,每个点都由值和指向下一个结点的地址组成的独立的单元,称为一个结点,有时候也称为节点,含义一样。

对于单链表,如果知道了第一个元素,就可以通过遍历访问整个链表,因此第一个结点,最重要,一般称为头结点。

2.2 虚拟结点

在做题以及在工程里经常会看到虚拟结点的概念,其实就是一个结点dummyNode,其next指针指向head,也就是dummyNode.next = head

因此,如果我们在算法里使用了虚拟结点,则要注意如果要获得head结点,或者从方法(函数)里返回的时候,则应使用dummyNode.next

另外注意,dummyNode的val不会被使用,初始化为0或者-1等都是可以的。既然值不会使用,那虚拟结点有啥用呢?

简单来说,就是为了方便我们处理首部结点,否则我们需要在代码里单独处理首部结点的问题。在链表反转里,我们会看到该方式可以大大降低解题难度。

3.创建链表

我们继续看如何构造链表。

首先要理解JVM是怎么构建出链表的,我们知道JVM里栈区和堆区,栈区主要存引用,也就是一个指向实际对象的地址,而堆区存的才是创建的对象,例如我们定义一个这样的表:

class Course;
	teacher = None
	student = None

这里的teacher和student就是指向堆的引用,假如我们这样定义:

class Counrsedef _init_(self,val):
		self.val = val
		self.next =None

这时候next就指向了下一个同为Course类型的对象了,例如:
在这里插入图片描述
这里通过栈中的引用(也就是地址)就可以找到val(1),然后val(1)结点又存了指向val(2)的地址,而val(3)又存了指向val(4)的地址,所以就构造出了一个链条访问结构。

在Java配套代码中BasicLink类,我们debug一下看一下从head开始next会发现是这样的:
在这里插入图片描述
这就是一个简单的线性访问了,所以链表就是从head开始,逐个开始向后访问,而每次所访问的对象的类型都是一样的。

根据面向对象的理论,在Java里规范的链表应该这么定义:

public class ListNode {
    private int data;
    private ListNode next;
    public ListNode(int data) {
        this.data = data;
    }
    public int getData() {
        return data;
    }
    public void setData(int data) {
        this.data = data;
    }
    public ListNode getNext() {
        return next;
    }
    public void setNext(ListNode next) {
        this.next = next;
    }
}

Python类与Java类具有相似的结构。
其中_init_方法用于初始化对象,
getData和setData方法用于获取和设置Data属性,
get_next和set_next方法用于获取和设置next属性。

请注意,Python类中的属性使用self.data和self.next的语法进行访问和设置的。


class ListNode:
    def __init__(self, data):
        self.data = data
        self.next = None

    def get_data(self):
        return self.data

    def set_data(self, data):
        self.data = data

    def get_next(self):
        return self.next

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

但是在LeeCode中算法题中经常使用这样的方式来创建链表:

class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next
listnode = ListNode(1)

这里的val就是当前结点的值,next指向下一个结点。因为两个变量都是public的,创建对象后能直接使用listnode.val和listnode.next来操作,虽然违背了面向对象的设计要求,但是上面的代码更为精简,因此在算法题目中应用广泛。

3.链表的增删改查

3.1 遍历链表

对于单链表,不管进行什么操作,一定从头开始逐个向后访问,所以操作之后是否还能找到表头非常重要

在这里插入图片描述

代码如下:

def length(self):
    """链表长度"""
    # cur初始时指向头节点
    cur = self._head
    count = 0
    # 尾节点指向None,当未到达尾部时
    while cur != None:
        count += 1
        # 将cur后移一个节点
        cur = cur.next
    return count

3.2 链表插入

单链表的插入,和数组的插入一样,过程不复杂,但是在编码时会发现处处是坑。单链表的插入操作需要要考虑三种情况:首部、中部和尾部。

(1)在链表的表头插入

链表表头插入新结点非常简单,容易出错的是经常会忘了head需要重新指向表头。我们创建一个新结点newNode,怎么连接到原来的链表上呢?

执行newNode.next=head即可。之后我们要遍历新链表就要从newNode开始一路next向下了是吧,但是我们还是习惯让head来表示,所以让head=newNode就行了,如下图:
在这里插入图片描述

(2) 在链表中间插入

在中间位置插入,我们必须先遍历找到要插入的位置,然后将当前位置接入到前驱动点和后继结点之间,但是到了该位置之后我们却不能获取前驱结点了,也就无法将结点介入进来了。这就好比一边过河一边拆桥,结果自己也回不去了(过河拆桥)。

为此,我们要在目标结点的前一个位置停下来,也就是使用cur.next的值而不是cur的值来判断,这是链表最常用的策略。

例如下图中,如果要在7的前面插入,当cur.next=node(7)了就应该停下来,此时cur.val=15.然后需要给newNode前后接两根线,此时只能先让new.next=node(15).next(图中虚线),然后node(15).next=new,而且顺序还不能错。

想一下为什么不能颠倒顺序?
由于每个节点都只有一个next,因此执行了node(15).next=new之后,结点15和7之间的连线就自动断开了,如图所示:
在这里插入图片描述

(3)在单链表的结尾插入结点

表尾插入就比较容易了,我们只要将尾结点指向新结点就行了。
在这里插入图片描述
综上,我们写出链表插入的方法如下所示:


# 指定位置添加
def insert(self, pos, item):
    """指定位置添加元素"""
    # 若指定位置pos为第一个元素之前,则执行头部插入
    if pos <= 0:
        self.add(item)
    # 若指定位置超过链表尾部,则执行尾部插入
    elif pos > (self.length() - 1):
        self.append(item)
    # 找到指定位置
    else:
        node = Node(item)
        count = 0
        # pre用来指向指定位置pos的前一个位置pos-1,初始从头节点开始移动到指定位置
        pre = self._head
        while count < (pos - 1):
            count += 1
            pre = pre.next
        # 先将新节点node的next指向插入位置的节点
        node.next = pre.next
        # 将插入位置的前一个节点的next指向新节点
        pre.next = node

这里需要再补充一点head =null的时候该执行什么操作呢?

如果是null的话,你要插入的结点就是链表的头结点,也可以直接抛出不能插入的异常,两种处理都可以,一般来说我们更倾向前者。

如果链表是单调递增的,一般会让你将元素插入到合适的位置,序列仍然保持单调,你可以尝试写一下该如何实现。

3.3 链表删除

删除同样分为在删除头部元素,删除中间元素和删除尾部元素。

(1)删除表头结点

删除表头元素还是比较简单的,一般只要执行head=head.next就行了。如下图,将head向前移动一次之后,原来的结点不可达,会被JVM回收掉。
在这里插入图片描述

(2)删除最后一个结点

删除的过程不算复杂,也是找到要删除的结点的前驱结点,这里同样要在提前一个位置判断,例如下图中删除40,其前驱结点为7。遍历的时候需要判断cur.next是否为40,如果是,则只要执行cur.next=null即可,此时结点40变得不可达,最终会被JVM回收掉。
在这里插入图片描述

(3)删除中间结点

删除中间结点时,也会要用cur.next来比较,找到位置后,将cur.next指针的值更新为cur.next.next就可以解决。如下图:
在这里插入图片描述

# 删除结点
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

同样,在很多算法中链表元素是有序的,此时如果要删除某个元素,则要先遍历链表,找到目标元素,然后再删除。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值