链表

单链表的概念

任何数据结构的基础都是创建+增删改查,由这几个操作可以构造很多算法题,所以我们也从这五项开始学习链表。

什么是链表

首先看一下什么是链表?使用链表存储数据,不强制要求数据在内存中集中存储,各个元素可以分散存储在内存中。例如,使用链表存储 {4,,15,,7,,40},各个元素在内存中的存储状态可能是:如下图:

链表

创建链表

JVM 里有栈区和堆区,栈区主要存引用,也就是一个指向实际对象的地址,而堆区存的才是创建的对象,例如我们定义这样一个类:

1
2
3
4
public class Course{
int val;
Course next;
}

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

Java 规范的链表定义

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
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;
    }
}

LeetCode 中的链表定义

1
2
3
4
5
6
7
8
9
public class ListNode{
  private int val;
  private ListNode next;
  ListNode(int x){
    val=x;
    next=null;
  }
}
ListNode listnode = new ListNode(1);

创建对象后能直接使用 listnode.val 和 listnode.next 来操作

链表的增删改查

遍历链表

对于单链表,不管进行什么操作,一定是从头开始逐个向后访问,所以操作之后是否还能找到表头非常重要。一定要注意"狗熊掰棒子"问题,也就是只顾当前位置而将标记表头的指针丢掉了。

1
2
3
4
5
6
7
8
9
public static int getListLength(Node head){
  int length = 0;
  Node node = head;
  while(node != null){
      length++;
      node = node.next;
  }
  return length;
}

链表插入

插入操作需要要考虑三种情况:首部、中部和尾部

  1. 在链表的表头插入 表头插入

  2. 在链表中间插入 表中插入

  3. 在单链表的结尾插入结点 表尾插入

完整实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/**
     * 链表插入
     * @param head       链表头节点
     * @param nodeInsert 待插入节点
     * @param position   待插入位置,从1开始
     * @return 插入后得到的链表头节点
     */
public static Node insertNode(Node head, Node nodeInsert, int position) {
    if (head == null) {
        //这里可以认为待插入的结点就是链表的头结点,也可以抛出不能插入的异常
        return nodeInsert;
    }
    //已经存放的元素个数
    int size = getLength(head);
    if (position > size+1  || position < 1) {
        System.out.println("位置参数越界");
        return head;
    }

    //表头插入
    if (position == 1) {
        nodeInsert.next = head;
        head = nodeInsert;
        return head;
    }

    Node pNode = head;
    int count = 1;
    //这里position被上面的size被限制住了,不用考虑pNode=null
    while (count < position - 1) {
        pNode = pNode.next;     //从head开始遍历,找到目的节点
        count++;
    }
    nodeInsert.next = pNode.next;
    pNode.next = nodeInsert;

    return head;
}

链表删除

删除操作也需要要考虑三种情况:首部、中部和尾部

  1. 在链表的表头删除 表头删除

  2. 在单链表的结尾删除 表尾删除

  3. 在单链表的中部删除结点 表中删除

完整实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/**
     * 删除节点
     * @param head     链表头节点
     * @param position 删除节点位置,取值从1开始
     * @return 删除后的链表头节点
     */
public static Node deleteNode(Node head, int position) {
    //判断链表是否空
    if (head == null) {
        return null;
    }
    int size = getListLength(head);
    //思考一下,这里为什么是size,而不是size+1(链表最长为size,删除不了size+1的结点)
    if (position > size || position <1) {
        System.out.println("输入的参数有误");
        return head;
    }
    if (position == 1) {
        //curNode就是链表的新head
        return head.next;
    } else {
        Node cur = head;
        int count = 1;
        while (count < position - 1) {
            cur = cur.next; //从head开始遍历,找到目的节点
            count++;
        }
        Node curNode = cur.next;
        cur.next = curNode.next;
        //上面两行可以直接简化成:cur.next=cur.next.next
    }
    return head;
}

问题

1.理解 Java 是如何构造出链表的
2.链表增加元素,首部、中间和尾部分别会有什么问题,该如何处理?
3.链表删除元素,首部、中间和尾部分别会有什么问题,该如何处理?
4.双向链表是如何构造的,如何实现元素的插入和删除。