单向链表详解

链表(Linked list)是一种物理存储单元上非连续的数据结构、数据元素的逻辑顺序是通过链表中的指针实现的。由于不必按顺序存储,链表在插入的时候可以达到 O(1) 的复杂度,但链表查找的时候由于不能通过索引查找,所以查找的时间复杂度是 O(n) 。常见的链表有单向链表,双向链表,以及环形链表。


单向链表

单向链表是最简单的一种链表,一般只包含存储的数据和指针(这里的指针不是 C 语言中的地址指针,他是下一个节点的引用),每个指针指向下一个节点,最后一个节点指向空,如下图所示。为了方便在尾部添加节点,我们新增一个 tail 变量,指向链表的尾节点。

alt

单向链表的节点类如下:

private class LinkNode {
    int val;// 节点中存储的数据。
    LinkNode next;// 指向下一个节点的指针。

    public LinkNode(int val, LinkNode next) {
        this.val = val;
        this.next = next;
    }
}

单向链表的插入

链表的插入分 3 种情况,一种是在头部插入,一种是在尾部插入,一种是在中间插入。在头部插入完之后要更新 head 节点,在尾部插入完之后要更新 tail 节点,如下图所示。

alt

插入节点之前必须要找到插入位置的前一个节点,来看下代码:

// 在链表中插入节点。
public void insert(int index, int val) {
    if (index == 0) {// 插入的是头节点。
        head = new LinkNode(val, head);// 创建头节点赋值给 head 。
        if (size == 0)// 如果之前链表为空,让 tail 也指向 head 。
            tail = head;
    } else if (index == size) {// 在链表末尾添加
        tail.next = new LinkNode(val, null);// 创建尾节点。
        tail = tail.next;// 更新尾节点。
    } else {// 在中间添加。
        LinkNode preNode = findPre(index);// 查找 index 节点的前一个节点。
        preNode.next = new LinkNode(val, preNode.next);// 添加新节点。
    }
    size++;// 节点个数加 1 。
}

对于链表的头和尾插入比较简单,我们来分析下链表中间节点的插入。

alt


单向链表的删除

链表的删除和插入类似,也分 3 种情况,如果是在头部或者尾部删除要记得更新 head 或者 tail 节点。在删除节点之前要找到待删除节点的前一个节点,让待删除节点的前一个节点指向待删除节点的下一个节点即可。

alt

来看下代码:

// 删除节点。
public int remove(int index) {
    if (size == 0)
        throw new IllegalStateException("链表是空的,没法删除……");
    LinkNode delete;// 待删除的节点。
    if (index == 0) {// 删除的是头节点。
        delete = head;// 记录待删除的节点。
        head = head.next;// 更新 head 节点。
        if (head == null)// 如果链表为空,让 tail 也为空。
            tail = null;
    } else {// 删除的不是头节点。
        LinkNode preNode = findPre(index);// 查找删除节点的前一个节点。
        if (index == size - 1) {// 删除的是尾节点。
            delete = preNode.next;
            preNode.next = null;// 删除尾节点。
            tail = preNode;// 更新尾节点。
        } else {// 删除的是中间节点。
            delete = preNode.next;
            preNode.next = delete.next;// 删除节点。
        }
    }
    delete.next = delete;// gc,自己指向自己。
    size--;// 节点个数减 1 。
    return delete.val;// 返回删除节点的值。
}

最后我们再来看下单链表的完整代码。

private LinkNode head;// 头节点。
private LinkNode tail;// 尾节点。
private int size;// 链表的长度。

// 在尾部添加节点。
public void add(int val) {
    insert(size, val);
}

// 删除头节点
public int poll() {
    return remove(0);
}

// 在链表中插入节点。
public void insert(int index, int val) {
    if (index == 0) {// 插入的是头节点。
        head = new LinkNode(val, head);// 创建头节点赋值给 head 。
        if (size == 0)// 如果之前链表为空,让 tail 也指向 head 。
            tail = head;
    } else if (index == size) {// 在链表末尾添加
        tail.next = new LinkNode(val, null);// 创建尾节点。
        tail = tail.next;// 更新尾节点。
    } else {// 不在末尾添加。
        LinkNode preNode = findPre(index);// 查找 index 节点的前一个节点。
        preNode.next = new LinkNode(val, preNode.next);// 添加新节点。
    }
    size++;// 节点个数加 1 。
}

// 删除节点。
public int remove(int index) {
    if (size == 0)
        throw new IllegalStateException("链表是空的,没法删除……");
    LinkNode delete;// 待删除的节点。
    if (index == 0) {// 删除的是头节点。
        delete = head;// 记录待删除的节点。
        head = head.next;// 更新 head 节点。
        if (head == null)// 如果链表为空,让 tail 也为空。
            tail = null;
    } else {// 删除的不是头节点。
        LinkNode preNode = findPre(index);// 查找删除节点的前一个节点。
        if (index == size - 1) {// 删除的是尾节点。
            delete = preNode.next;
            preNode.next = null;// 删除尾节点。
            tail = preNode;// 更新尾节点。
        } else {// 删除的是中间节点。
            delete = preNode.next;
            preNode.next = delete.next;// 删除节点。
        }
    }
    delete.next = delete;// gc,自己指向自己。
    size--;// 节点个数减 1 。
    return delete.val;// 返回删除节点的值。
}

// 查找 index 节点的前一个节点。
LinkNode findPre(int index) {
    // index 必须是有效的,不能越界。查找前一个,index可以等于size。
    if (index < 0 || index > size)
        throw new IndexOutOfBoundsException("越界了……");
    if (index == 0)// 头节点的前一个是空节点。
        return null;
    LinkNode preNode = head;
    for (int i = 0; i < index - 1; i++)
        preNode = preNode.next;
    return preNode;
}

// 节点的个数。
public int size() {
    return size;
}

// 清空链表。
public void clear() {
    while (head != null) {
        LinkNode next = head.next;
        head.next = head;// 自己指向自己。
        head = next;
    }
    tail = null;
    size = 0;
}

上面代码在节点删除和添加的时候有可能会导致头节点不停的变动,我们还可以在链表的前面添加一个哑节点 dummy ,这个哑节点不存储任何数据,他的 next 指针始终指向原链表的头节点,如下图所示。

alt

这样在添加和删除的时候就不需要在判断是否是头节点了,来看下代码:

private LinkNode dummyNode = new LinkNode(-1, null);// 哑节点。
private int size;// 链表的长度。

// 在尾部添加节点。
public void add(int val) {
    insert(size, val);
}

// 删除头节点
public int poll() {
    return remove(0);
}

// 在链表中插入节点。
public void insert(int index, int val) {
    LinkNode preNode = findPre(index);// 查找 index 节点的前一个节点。
    preNode.next = new LinkNode(val, preNode.next);// 添加新节点。
    size++;// 节点个数加 1 。
}

// 删除节点。
public int remove(int index) {
    if (size == 0)
        throw new IllegalStateException("链表是空的,没法删除……");
    LinkNode preNode = findPre(index);// 查找删除节点的前一个节点。
    LinkNode delete = preNode.next;// 记录待删除的节点。
    preNode.next = delete.next;// 删除节点。
    delete.next = delete;// gc,自己指向自己。
    size--;// 节点个数减 1 。
    return delete.val;// 返回删除节点的值。
}

// 查找 index 节点的前一个节点。
LinkNode findPre(int index) {
    // index 必须是有效的,不能越界。查找前一个,index可以等于size。
    if (index < 0 || index > size)
        throw new IndexOutOfBoundsException("越界了……");
    LinkNode preNode = dummyNode;
    for (int i = 0; i < index; i++)
        preNode = preNode.next;
    return preNode;
}

// 节点的个数。
public int size() {
    return size;
}

// 清空链表。
public void clear() {
    LinkNode cur = dummyNode.next;
    while (cur != null) {
        LinkNode next = cur.next;
        cur.next = cur;// 自己指向自己。
        cur = next;
    }
    dummyNode.next = null;
    size = 0;
}

原文链接

如果觉得有用就给个赞吧,还可以关注我的《牛客博客》查看更多的详细题解

#链表##数据结构和算法##数据结构##链表类型##单向链表#
常见数据结构介绍 文章被收录于专栏

主要介绍一些常见的数据结构,包括数组,滚动数组,差分数组,树状数组,单向链表,双向链表,循环链表,跳表,异或链表,队列,循环队列,双端队列,栈,散列表,堆,二叉树,二叉搜索树,AVL树,红黑树,字典树,哈夫曼树,线段树,笛卡尔树,图的介绍,图的遍历,Dijkstra算法,Bellman-Ford算法,SPFA算法,Floyd算法,Prim算法,Kruskal算法,Boruvka算法,拓扑排序等。

全部评论
力扣那个数据结构是不是也是你?
点赞 回复 分享
发布于 2023-06-26 13:05 河南

相关推荐

11-24 00:11
已编辑
广东工业大学 算法工程师
避雷深圳&nbsp;&nbsp;yidao,试用期&nbsp;6&nbsp;个月。好嘛,试用期还没结束,就直接告诉你尽快找下一家吧,我谢谢您嘞
牛客75408465号:笑死,直属领导和 hr 口径都没统一,各自说了一些离谱的被裁理由,你们能不能认真一点呀,哈哈哈哈哈😅😅😅
点赞 评论 收藏
分享
拒绝无效加班的小师弟很中意你:求职意向没有,年龄、课程冗余信息可以删掉,需要提升项目经历。排版需要修改。
点赞 评论 收藏
分享
10-21 23:48
蚌埠坦克学院
csgq:可能没hc了 昨天一面完秒挂
点赞 评论 收藏
分享
评论
3
2
分享
牛客网
牛客企业服务