数据结构链表看这一篇就够了 Link List 手写实现(图文解释,附常见面试题)

目录

链表数据结构

链表分类类型

1. 单向链表

2. 双向链表

3. 循环链表

✍️实现一个链表

1. 链表节点

2. 头插节点

3. 尾插节点

4. 拆链操作

5. 删除节点

6. 按照index查询对象

7. 打印

链表使用测试

️常见面试问题


链表数据结构

在计算机科学中,链表是数据元素的线性集合,元素的线性顺序不是由它们在内存中的物理地址给出的。它是由一组节点组成的数据结构,每个元素指向下一个元素,这些节点一起,表示线性序列。

数据结构链表看这一篇就够了 Link List 手写实现(图文解释,附常见面试题)_第1张图片

在最简单的链表结构下,每个节点由数据和指针(存放指向下一个节点的指针)两部分组成,这种数据结构允许在迭代时有效地从序列中的任何位置插入或删除元素。

链表的数据结构通过链的连接方式,提供了可以不需要扩容空间就更高效的插入和删除元素的操作,在适合的场景下它是一种非常方便的数据结构。但在一些需要遍历、指定位置操作、或者访问任意元素下,是需要循环遍历的,这将导致时间复杂度的提升。

看了这些还是很懵,下面手写个双向链表很快就清楚链表结构啦

链表分类类型

链表的主要表现形式分为;单向链表、双向链表、循环链表,接下来我们分别介绍下。

1. 单向链表

单链表包含具有数据字段的节点以及指向节点行中的下一个节点的“下一个”字段。可以对单链表执行的操作包括插入、删除和遍历。

数据结构链表看这一篇就够了 Link List 手写实现(图文解释,附常见面试题)_第2张图片

2. 双向链表

在“双向链表”中,除了下一个节点链接之外,每个节点还包含指向序列中“前一个”节点的第二个链接字段。这两个链接可以称为'forward('s')和'backwards',或'next'和'prev'('previous')。

数据结构链表看这一篇就够了 Link List 手写实现(图文解释,附常见面试题)_第3张图片

3. 循环链表

在列表的最后一个节点中,链接字段通常包含一个空引用,一个特殊的值用于指示缺少进一步的节点。一个不太常见的约定是让它指向列表的第一个节点。在这种情况下,列表被称为“循环”或“循环链接”;否则,它被称为“开放”或“线性”。它是一个列表,其中最后一个指针指向第一个节点。

数据结构链表看这一篇就够了 Link List 手写实现(图文解释,附常见面试题)_第4张图片

✍️实现一个链表

简化结构的方式把 LinkedList 手写实现

先看看我们要实现的一些链表方法吧

package main.pjp.linked_List;

public interface PjpList {
    // 添加
    boolean add(E e);
    
    // 头插
    boolean addFirst(E e);

    // 尾插
    boolean addLast(E e);

    // 删除
    boolean remove(Object o);

    // 按照index查询对象
    E get(int index);

    // 打印
    void printLinkList();
}

然后建个实现类开始实现这些方法

package main.pjp.linked_List;

public class PjpLinkedList implements PjpList {}

1. 链表节点

/**
 * ?表示不确定的 java 类型
 * T (type) 表示具体的一个 java 类型
 * K V (key value) 分别代表 java 键值中的Key Value
 * E (element) 代表 Element
 */
private static class Node {

    E item; // 值
    Node next; // 后继节点
    Node prev; // 前驱节点

    public Node(Node prev, E element, Node next) {
        this.item = element;
        this.next = next;
        this.prev = prev;
    }

}
  • 链表的数据结构核心根基就在于节点对象的使用,并在节点对象中关联当前节点的上一个和下一个节点。通过这样的方式构建出链表结构。
  • 但也因为在链表上添加每个元素的时候,都需要创建新的 Node 节点,所以这也是一部分耗时的操作。

2. 头插节点

void linkFirst(E e) {
final Node f = first; // 获取双向链表的第一个元素
final Node newNode = new Node<>(null, e, f); // 新初始化一个元素,并将它的后继节点指向原链表的第一元素
first = newNode; // 插入的元素变成第一个元素
if (f == null) {
    last = newNode;
} else {
    f.prev = newNode; // 原第一个元素的前驱节点指向插入元素
}
size++;
}

数据结构链表看这一篇就够了 Link List 手写实现(图文解释,附常见面试题)_第5张图片

  • 头插的操作流程,先把头节点记录下来。之后创建一个新的节点,新的节点构造函数的头节点入参为null,通过这样的方式构建出一个新的头节点。
  • 原来的头结点,设置 f.prev 连接到新的头节点,这样的就可以完成头插的操作了。另外如果原来就没有头节点,头节点设置为新的节点即可。最后记录当前链表中节点的数量,也就是你使用 LinkedList 获取 size 时候就是从这个值获取的。

3. 尾插节点

void linkLast(E e) {
final Node l = last;
final Node newNode = new Node<>(l, e, null);
last = newNode;
if (l == null) {
    first = newNode;
} else {
    l.next = newNode;
}
size++;
}

数据结构链表看这一篇就够了 Link List 手写实现(图文解释,附常见面试题)_第6张图片

  • 尾差节点与头插节点正好相反,通过记录当前的结尾节点,创建新的节点,并把当前的结尾节点,通过 l.next 关联到新创建的节点上。同时记录 size 节点数量值。

4. 拆链操作

E unlink(Node a) {
    final E element = a.item; // 值
    final Node next = a.next; // 后一个元素
    final Node prev = a.prev; // 前一个元素

    if (prev == null) { // 删除的是第一个元素
        first = next; // 直接让下一个元素成为第一个元素
    } else { // 删除的不是第一个元素
        prev.next = next; // 前一个元素的后继节点直接指向后一个元素
        a.prev = null;
    }
    if (next == null) { // 删除的是最后一个元素
        last = prev;
    } else {
        next.prev = prev; // 后一个元素的前继节点直接指向前一个元素
        a.next = null;  // 和前面 a.prev = null; 配合前后节点都置为null,真正剔除删除元素
    }
    a.item = null;
    size--;
    return element;
}

数据结构链表看这一篇就够了 Link List 手写实现(图文解释,附常见面试题)_第7张图片

  • unlink 是一种拆链操作,只要你给定一个元素,它就可以把当前这个元素的上一个节点和一个节点进行相连,之后把自己拆除。
  • 这个方法常用于 remove 移除元素操作,因为整个操作过程不需要遍历,拆除元素后也不需要复制新的空间,所以时间复杂度为 O(1)

5. 删除节点

public boolean remove(Object o) {
if (o == null) {
    for (Node a = first; a != null; a = a.next) {
        if (a.item == null) {
            unlink(a);
            return true;
        }
    }
} else {
    for (Node a = first; a != null; a = a.next) {
        if (o.equals(a.item)) {
            unlink(a);
            return true;
        }
    }
}
return false;
}

数据结构链表看这一篇就够了 Link List 手写实现(图文解释,附常见面试题)_第8张图片

  • 删除元素的过程需要 for 循环判断比删除元素的值,找到对应的元素,进行删除。
  • 循环比对的过程是一个 O(n) 的操作,删除的过程是一个 O(1) 的操作。所以如果这个链表较大,删除的元素又都是贴近结尾,那么这个循环比对的过程也是比较耗时的。

6. 按照index查询对象

public E get(int index) {
return node(index).item;
}

Node node(int index) {
if (index < (size >> 1)) {
    Node f = first;
    for (int i = 0; i < index; i++) {
        f = f.next;
    }
    return f;
} else {
    Node l = last;
    for (int i = size - 1; i > index; i--) {
        l = l.prev;
    }
    return l;
}
}

为了提高效率,做了以下操作

  • 如果index在整个链表的前部分,则从first,向后查询
  • 如果index在整个链表的后部分,则从last,向前查询

7. 打印

public void printLinkList() {
    if (size == 0) {
        System.out.println("链表为空");
    } else {
        Node temp = first;
        System.out.print("目前的列表,头节点:" + first.item + " 尾节点:" + last.item + " 整体:");
        while (temp != null) {
            System.out.print(temp.item + ",");
            temp = temp.next;
        }
        System.out.println();
    }
}

链表使用测试

package main.pjp.test;

import main.pjp.linked_List.PjpLinkedList;
import main.pjp.linked_List.PjpList;

public class test {
    public static void main(String[] args) {
        PjpList list = new PjpLinkedList<>();
        // 添加元素
        list.add("a");
        list.addFirst("b");
        list.addLast("c");
        // 打印列表
        list.printLinkList();
        // 头插元素
        list.addFirst("d");
        // 删除元素
        list.remove("b");
        // 打印列表
        list.printLinkList();
        System.out.println(list.get(2));
    }
}

测试结果

目前的列表,头节点:b 尾节点:c 整体:b,a,c,
目前的列表,头节点:d 尾节点:c 整体:d,a,c,
c

️常见面试问题

1. 描述一下链表的数据结构?

一组称为节点的对象组成,每个节点包含两个部分:数据元素和指针。链表中的节点通过指针        连接起来,形成有序的数据序列。

2. Java 中 LinkedList 使用的是单向链表、双向链表还是循环链表?

采用的是循环双向链表数据结构,链表的最后一个节点的next指向第一个节点。

3. 链表中数据的插入、删除、获取元素,时间复杂度是多少?

  • 头部插入/删除:只需要修改头结点的指针即可完成插入/删除操作,因此时间复杂度为 O(1)。
  • 尾部插入/删除:只需要修改尾结点的指针即可完成插入/删除操作,因此时间复杂度为 O(1)。
  • 指定位置插入/删除:需要先移动到指定位置,再修改指定节点的指针完成插入/删除,因此需要移动平均 n/2 个元素,时间复杂度为 O(n)。
  • 获取元素,时间复杂度 O(n)。

4. 什么场景下使用链表更合适

链表最适合在需要频繁进行插入和删除操作的场景下使用;不适用于频繁访问元素,链表访问元素的时间复杂度为 O(n)

你可能感兴趣的:(链表,list,数据结构,算法)