链表&设计链表

本文介绍了链表的基本概念,包括单向、双向和循环链表,以及链表常见的操作如增删改查、反转,特别强调了哑节点在链表操作中的作用。同时,通过实例展示了如何使用快慢指针删除倒数第N个节点的方法。
摘要由CSDN通过智能技术生成

链表

链表是一种常见的基础数据结构,是一种线性表,但是并不会按线性的顺序存储数据,而是在每一个节点里存到下一个节点的指针。由于不必须按顺序存储,链表在插入的时候可以达到O(1)复杂度,比另一种线性表顺序表快得多,但是查找一个节点或者访问特定编号的节点则需要O(n)的时间,而顺序表相应的时间复杂度分别是O(logn)和O(1)。

链表结构种类

从结构上分链表大体可分为单向链表、双向链表、循环链表。

单向链表

   单向链表分为两部分,一个是当前节点的值,另外一个是指向下一个节点的链接。

双向链表

   双向链表分为三部分,数值、向前的节点链接、向后的节点链接。

循环链表

    将单向链表首尾相连即可构成一个循环链表。

链表题型

  • 链表增删改查的操作
  • 链表反转(递归法、迭代法)
  • 合并链表

链表解题归纳

  • 链表在操作时候一般会使用一个哑节点
    • 哑节点可以记录链表的头部指针,单向链表只记录了next节点的位置
    • 方便操作,数组的下标是从0开始的。链表在操作时候把哑节点作为下标为0的节点,可以和大家日常使用数组习惯一致
  • 链表中会出现head.Next这种用法,有时候容易绕晕了。这时候记住一条head.Next看作一个整体,如果怕记乱可以多写一步用变量替换法
例如:删除head节点的下一个节点。找到下个节点指向的位置,让head指向下个节点指向的位置即可
1. 找到下个节点,声明tmp为head下个节点
tmp := head.Next
2. 让head指向tmp的下个节点
head.Next = tmp.Next
  • 链表操作时候,一般先找到待操作节点的前一个位置

707. 设计链表

type MyLinkedList struct {
    size int         // 维护一个链表长度,对于无效的下标操作不用遍历链表
    head *ListNode
}

func Constructor() MyLinkedList {
    return MyLinkedList{
        head: &ListNode{},     // 链表操作时候一般会使用一个哑节点
        size: 0,
    }
}


func (l *MyLinkedList) Get(index int) int {
    if index < 0 || index >= l.size {
        return -1
    }
    cur := l.head // 可以把这里理解为数组的下标0位置,方便操作
    // 1. 首先找到待操作位置
    for i := 0; i<=index; i++ {
        cur = cur.Next
    }
    // 2. 返回查询结果
    return cur.Val
}


func (l *MyLinkedList) AddAtHead(val int)  {
    l.AddAtIndex(0, val)
}


func (l *MyLinkedList) AddAtTail(val int)  {
    l.AddAtIndex(l.size, val)
}


func (l *MyLinkedList) AddAtIndex(index int, val int)  {
    if index > l.size {
        return
    }
    if index < 0 {
        index = 0
    }
    cur := l.head
    // 1. 找到待操作位置
    // 这里条件是i<index。此时下标是(index-1),注意观察题目说的是之前还是之后,加上新插入节点恰好下标是index
    for i := 0; i < index; i++ {
        cur = cur.Next
    }
    // 2. 生成新节点
    node := &ListNode{
        Val: val,
        Next: cur.Next,
    }
    // 3. 插入到链表中
    cur.Next = node
    // 4. 链表长度记得+1
    l.size++
}


func (l *MyLinkedList) DeleteAtIndex(index int)  {
    // 删除最后一个元素没有意义,所以index也不能等有l.size
    if index < 0 || index >= l.size {
        return
    }
    l.size--
    cur := l.head
    // 1. 找到待操作位置
    for i := 0; i<index; i++ {
        cur = cur.Next
    }
    // 2. 删除节点
    cur.Next = cur.Next.Next

}
    

/**
 * Your MyLinkedList object will be instantiated and called as such:
 * obj := Constructor();
 * param_1 := obj.Get(index);
 * obj.AddAtHead(val);
 * obj.AddAtTail(val);
 * obj.AddAtIndex(index,val);
 * obj.DeleteAtIndex(index);
 */

203. 移除链表元素

解题思路:

  • 找到符合条件的节点,代码上的实现就是(cur.Val == val)
  • 让符合条件的节点前一个节点直接指向当前节点的下一个节点即可
  • 由于单向链表只记录了next节点位置,这里我们还要维护一个pre节点,记录前一个节点

定义一个哑节点,让哑节点指向链表头部节点,因为会对头节点做for循环迭代,需要保存记录头节点位置

func removeElements(head *ListNode, val int) *ListNode {
    // 定义哑节点,记录链表的头指针
    dummy := &ListNode{}
    dummy.Next = head
    // 记录上一个节点,找到满足条件的节点时候需要知道前一个节点
    pre := dummy
    // 当前节点
    cur := dummy.Next
    for cur != nil {
        if cur.Val == val {
            pre.Next = cur.Next
        } else {
            pre = cur
        }
        cur = cur.Next
    }
    return dummy.Next
}

206. 反转链表

解题思路:

假如你左手拿着📱,右手拿着🍗。现在你想右手拿📱,左手拿🍗。你一定是先把手机放下或者把鸡腿放下,或者都放下。。。

两个变量交换肯定要借助于第三个变量

循环遍历链表,按照以下顺序一个节点一个节点反转

  • 保存下个节点位置(把手机放到桌子上
  • 反转当前节点,让当前节点指向pre(上个节点)。(右手的鸡腿🍗给左手
  • 当前节点反转完成,更新pre节点(当前节点变为了pre节点)。
  • 移动到下一个节点(从桌子上拿起🍗
func reverseList(head *ListNode) *ListNode {
    var pre *ListNode
    for head != nil {
        // 保存下个节点指针
        next := head.Next
        // 当前节点反转,指向前一个节点
        head.Next = pre
        // 当前节点完成操作,pre前移一步(当前节点变为pre节点)
        pre = head
        // head移向下一个操作节点
        head = next
    }
    return pre
}

24. 两两交换链表中的节点

解题思路:

把前两个节点位置交换写出来就可以了,后面节点都是循环的。由于第一个节点是没有上一个节点的,这里我们可以使用哑节点这样就和后面的保持一致了

  • pre节点指向新的节点
  • node1指向node3节点
  • node2指向node1节点
  • 当前节点后移

func swapPairs(head *ListNode) *ListNode {
    dummy := &ListNode{0, head}
    cur := dummy
    for cur.Next != nil && cur.Next.Next != nil {
        node1 := cur.Next
        node2 := cur.Next.Next
        cur.Next = node2
        node1.Next = node2.Next
        node2.Next = node1
        cur = node1
    }
    return dummy.Next
}

19. 删除链表的倒数第 N 个结点

解题思路:

使用快慢指针,即先让快指针走n个节点和头部保持n,此时慢指针开始。等快指针遍历完链表时候,此时慢指针指向的位置即是我们要操作目标节点的前一个节点

整体思路不难,但是在遍历时候有些小技巧和边界条件理解起来比较麻烦。下面结合代码整体看下。

func removeNthFromEnd(head *ListNode, n int) *ListNode {
    // 声明一个哑节点,方便操作。对于只有一个元素的情况也不用特殊处理了
    dummy := &ListNode{0, head}
    // 快慢指针,注意这里慢指针是从哑节点开始的,会影响到下面找待操作位置的条件
    fast, slow := head, dummy
    // i<n 由于数组下标是从0开始,题目中限制了n>=1。我们平时说的倒数第n个在计数时候是从1开始的
    for i := 0; i<n; i++ {
        fast = fast.Next
    }

    // 这里为什么不用fast.Next != nil的条件呢,因为我们加入了哑节点,所以要让slow节点多移动一步
    // 此时slow所在的位置恰好是待操作节点的前一个位置
    for fast != nil  {
        fast = fast.Next
        slow = slow.Next
    }
    // 哑节点加入可以省去对一个元素的特殊情况的判空处理
    slow.Next = slow.Next.Next
    return dummy.Next
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值