实现一个属于自己的链表类

一、链表的基本方法

链表是一种通过节点存储元素,并且节点与节点之间是连接起来的数据结构。

0->1->2->3->4->5->NULL

  • 对于链表,如果要想访问存在这个链表中的所有节点,相应的必须把链表的头给存储起来。通常链表的头叫做head,即在链表LinkedList类中,应该又一个Node型的变量head,它指向链表中的第一个节点。private Node head;
  • 需要设置一个私有的size来记录链表中元素的个数,这个size只能在类内操作,不允许外部进行修改。
  • 添加查看链表中元素个数和判断链表是否为空的方法
// 支持泛型
public class LinkedList<E> {
    // 节点设计成链表类中的内部类
    // 设计私有类,只有在链表数据结构内才可以访问到Node
    private class Node {
        // 设计成public 在LinkedList中可以随意访问操作 不需要设置get,set方法
        public E e;   // 存放元素
        public Node next;  // 指向Node的引用

        public Node(E e, Node next) {
            // 将用户传来的数据交给节点
            this.e = e;
            this.next = next;
        }

        // 用户只传来e
        public Node(E e) {
            this(e, null);
        }

        // 用户什么都不传
        public Node() {
            this(null, null);
        }
        // 对每一个节点设置toString方法
        @Override
        public String toString() {
            // 每一个节点,直接打印e所对应的toString
            return e.toString();
        }
    }
    
    // 第一个节点
    private Node head;
    // 用户不能在外部直接修改size
    private int size;  // 来记录链表中有多少个元素

    // 链表构造函数
    public LinkedList() {
        // 对于一个空的链表来说,它是存在一个节点的,这个节点就是唯一的虚拟头节点
        head = null;
        size = 0;
    }

    // 获取链表中元素的个数
    public int getSize() {
        return size;
    }

    // 返回链表是否为空
    public boolean isEmpty() {
        // size是否为0
        return size == 0;
    }
    // 获取链表中元素的个数
	public int getSize() {
	    return size;
	}
	
	// 返回链表是否为空
	public boolean isEmpty() {
	    // size是否为0
	    return size == 0;
	}
}

二、在链表头添加元素

1. 与数组的对比

对于数组,向数组尾添加一个元素非常的方便。相反,对于链表来说,在链表的头部添加元素是非常方便的。
这是因为在数组中,size直接指向了数组中最后一个元素的下一个位置,它跟踪数组的尾巴。而对于链表,设立了链表的头head,有一个变量跟踪链表的头,没有相应的变量去跟踪链表的尾部,所以在链表的头部添加元素非常方便。

2. 添加的具体过程

假设将新的元素10添加进0->1->2->3->4->5->NULL这个链表中,首选需要把元素10放进一个节点里,这个节点里存储了元素10以及相应的next。添加的关键在于如何将这个节点挂接到链表中,同时不破坏现有链表的结构。

  • 将元素10这个节点(Node)的next指向现在链表的头部(head)也就是node.next=head
  • 此时的链表已经变成了10->0->1->2->3->4->5->NULL,存储10的节点成了新的头部。
  • 接下来需要维护一下head,让head=node,即让head指向新的10的节点。这样便完成了将10插入到链表的头部。
  • 整个过程是在一个函数中执行的,对于node这个变量来说,指向的是“10”这个节点,在函数结束之后,node的块作用域也就结束了,node变量相应的也就没用了。

LinkedList中添加一个新的方法,作用在链表头添加新的元素。

public void addFirst(E e) {
    // 首先创建一个新的节点 此时next为空
    Node node = new Node(e);
    // 让node的next指向head
    node.next = head;
    // 更新一下head为新的node元素
    head = node;
    // 之后维护一下size
    size++;
}

其中也可以将逻辑的直接一行表示,让head直接指向新的节点

public void addFirst(E e) {
    // 首先new一个新的Node,传入e,Node直接指向当前链表的head,然后将Node赋值给head进行更新
    head = new Node(e, head);
    // 之后维护一下size
    size++;
}

三、在链表中插入元素

1. 插入元素的具体逻辑

已有链表0->1->2->3->4->5->NULL
在链表“索引”为2的地方添加一个新的元素9,对于链表来说没有索引的概念,具体来说是在当前节点1的位置之后添加一个新的元素9,之后是节点2。

  • 首先创建“9”的节点
  • 要想把“9”插入到正确的位置,就必须要找到插入“9”节点之后,这个“9”节点之前的节点是谁。把它称做prev。prev的初始化适合head在同一个地方。
  • 在这个例子中,需要先找到“1”节点让它的next指向“9”,再将“9”的next指向原本“1”节点之后节点“2”
  • 当前需要搜索插入“9”之前的节点是谁,因为插入了“9”之后索引为2,那么插入“9”之前的节点索引为1。这是就需要从0开始遍历,遍历到索引为1的位置就可以了,相应的维护prev找到1的位置。

一旦找到对应位置1后

  1. 首先将node的next指向prev的下一个元素,node.next = prev.next;在这里prev.next是插入位置的后一个节点
  2. 之后让prev的next等于nodeprev.next = node;在这里,prev.next指向新的node节点
    (这两点的顺序是不允许改变的,如果调换顺序执行,prev.next已经赋值node了,再执行node.next = prev.next,实际是让它又指向了自己。顺序是非常重要的
Node node = new Node(e);
node.next = prev.next;
prev.next = node;

也可以进行相应的缩写

prev.next = new Node(e, prev.next);

这个过程的关键,在于找到待添加节点的前一个节点,也是prev变量进行的事情。但值得注意的是:如果想把新的元素,添加在“索引”为0的地方,即添加在链表的头部,链表头部的节点是不存在前一个节点的。对于这种情况需要特殊处理,就是前面一节提到的往链表头部添加新的元素。

2. 实现添加元素的方法

// 链表的index添加新的元素e
// 真正使用链表很少有这样的操作,当选择使用链表的时候,通常选择不使用"索引"
public void add(int index, E e) {
    // 需要先判断index的合法性
    if (index < 0 || index > size) {
        throw new IllegalArgumentException("Add failed.Illegal index.");
    }
    if (index == 0) {
        addFirst(e);
    } else {
        // 设计一个Node从head开始
        Node prev = head;
        for (int i = 0; i < index - 1; i++) {
            // 当前prev存的节点的下一个节点放进prev中
            // 遍历一直挪动prev位置存放下一个节点
            prev = prev.next;
        }
        // 插入过程
        prev.next = new Node(e, prev.next);
        // 插入之后维护size
        size++;
	}
}

3. 相应实现末尾添加

// 在链表的末尾添加一个新的元素e
public void addLast(E e) {
    // 复用add(),只需要在size添加即可
    add(size, e);
}

四、为链表设立虚拟头结点

1. 链表头添加元素时的特殊性

在链表添加元素的过程中,在向链表任意一个地方添加元素的时候,在链表头添加元素和在链表其他位置添加元素的逻辑是不一样的,因为链表头没有前一个元素的节点,所以需要进行特殊处理。

2. 解决办法

可以造一个链表头之前的节点,这个节点不存储任意的元素
null->0->1->2->3->4->5->NULL
将这个空节点称之为整个链表真正的head,通常叫做dummyHead,即虚拟头节点。这样,链表的第一个元素是dummyHead.next所对应的元素。dummyHead位置的元素是根本不存在的,对用户来讲也根本没有意义,它是0节点之前的虚拟元素。

3. 具体的逻辑实现

在LinkedList()中,head存放一个具体的元素,初始化时是null。引入虚拟头节点,则private Node dummyHead
构造函数初始化时,则需要引人一个节点

public LinkedList() {
    // 对于一个空的链表来说,它是存在一个节点的,这个节点就是唯一的虚拟头节点
    dummyHead = new Node(null, null);
    size = 0;
}

对于add()添加操作也需要进行相应的改动,不再需要判断index==0的情况

public void add(int index, E e) {
    // 需要先判断index的合法性
    if (index < 0 || index > size) {
        throw new IllegalArgumentException("Add failed.Illegal index.");
    }
    // 此时dummyHead指向的是0元素之前一个的位置的节点
    Node prev = dummyHead;
    // 只需要遍历到index就可以了,因为是从dummyHead开始遍历的
    for (int i = 0; i < index; i++) {
        // 当前prev存的节点的下一个节点放进prev中
        // 遍历一直挪动prev位置存放下一个节点
        prev = prev.next;
    }

    prev.next = new Node(e, prev.next);
    // 插入之后维护size
    size++;
}

如此一来,在链表的头部添加一个元素可以复用add()方法

// 在链表头添加新的元素
public void addFirst(E e) {
    add(0, e);
}

五、遍历、查询、修改

1. 按照“索引”查找元素

这里需要注意需要查找index元素,应该从整个链表的第一个元素开始遍历,即dummyHead.next开始遍历。

// 查询操作
public E get(int index) {
    // get之前先判断合法性
    if (index < 0 || index >= size) {
        throw new IllegalArgumentException("Add failed.Illegal index.");
    }

    // 遍历链表,是从索引为0开始的,从当前的开始遍历
    Node cur = dummyHead.next;
    for (int i = 0; i < index; i++)
        cur = cur.next;
    // 最终cur里存储的e就是需要查找的元素
    return cur.e;
}

// 获取链表第一个元素
public E getFirst() {
    return get(0);
}

// 获取链表最后第一个元素
public E getLast() {
    return get(size - 1);
}

2. 更新链表元素

// 链表的更新,修改
public void set(int index, E e) {
    // 判断合法
    if (index < 0 || index >= size) {
        throw new IllegalArgumentException("Add failed.Illegal index.");
    }
    // 进行一次遍历,找到第index元素进行替换
    Node cur = dummyHead.next;
    for (int i = 0; i < index; i++)
        cur = cur.next;
    // 最终cur里存储的e等于新的e
    cur.e = e;
}

3. 查找链表中是否有元素e

这里的遍历和以前的查询操作遍历有所不同,之前的遍历都有索引,知道自己要查询或者修改的是链表中的第几个节点,而这里需要从头对链表都进行一次遍历。

// 查找链表中是否存在元素e
public boolean contains(E e) {
    // 设置cur从第一个节点开始
    Node cur = dummyHead.next;
    // 不知道循环多少次使用while
    // cur 节点不等他null的话,意味着当前cur节点是一个有效节点
    while (cur != null) {
        // cur.e是用户传来的e,返回true
        if (cur.e.equals(e))
            return true;
        // 否则就看下一个节点
        cur = cur.next;
        // 直到cur为空,说明把整个链表遍历了一遍
    }
    return false;
}

六、从链表中删除元素

1. 删除逻辑

依然使用有虚拟头节点的链表,删除的节点为取名为delNode
null->0->1->2->3->4->5->NULL
假设删除“索引”为2的位置的元素,其实就是删除节点2元素。

  • 首先找到待删除元素的前一个元素节点1用prev进行标注
  • 此时要删除的节点2就是prev.next对应的节点
  • 将prev节点的next的指针指向要删除节点2的下一个节点3,即是将“1的节点”指向“3的节点”。具体的操作为prev.next = delNode.next;
  • 此时null->0->1->2->3->4->5->NULL相当于1直接连接到3节点看似删除了,2节点其实并没有删除
  • 为了方便Java能够回收2节点的空间,还需要让2节点的next和整个链表脱离,即delNode.next = null。运行之后才算真正的删除。

需要删除元素,必须找到next产生变化

2. 实现删除方法remove()

// 删除链表中index位置元素 返回删除的元素
public E remove(int index) {
    // 判断合法
    if (index < 0 || index >= size) {
        throw new IllegalArgumentException("Add failed.Illegal index.");
    }

    // prev存了待删除节点之前的节点
    Node prev = dummyHead;
    for (int i = 0; i < index; i++) {
        prev = prev.next;
    }
    // 将删除的节点保存进retNode;
    Node retNode = prev.next;
    // 将当前的前一个元素的节点指向当前后一个节点
    prev.next = retNode.next;
    // 让删除元素的节点的指向为空
    retNode.next = null;
    // 维护size
    size--;
    // 将删除的元素返回
    return retNode.e;
}

// 删除第一个元素,返回删除的元素
public E removeFirst() {
    return remove(0);
}

// 删除第一个元素,返回删除的元素
public E removeLast() {
    return remove(size - 1);
}

七、完整的链表类LinkedList

LinkedList.java

// 支持泛型
public class LinkedList<E> {
    // 节点设计成链表类中的内部类
    // 设计私有类,只有在链表数据结构内才可以访问到Node
    private class Node {
        // 设计成public 在LinkedList中可以随意访问操作 不需要设置get,set方法
        public E e;   // 存放元素
        public Node next;  // 指向Node的引用

        public Node(E e, Node next) {
            // 将用户传来的数据交给节点
            this.e = e;
            this.next = next;
        }

        // 用户只传来e
        public Node(E e) {
            this(e, null);
        }

        // 用户什么都不传
        public Node() {
            this(null, null);
        }

        // 对每一个节点设置toString方法
        @Override
        public String toString() {
            // 每一个节点,直接打印e所对应的toString
            return e.toString();
        }
    }

    // 修改为dummyHead设置虚拟头节点
    private Node dummyHead;
    // 用户不能在外部直接修改size
    private int size;  // 来记录链表中有多少个元素

    // 链表构造函数
    public LinkedList() {
        // 对于一个空的链表来说,它是存在一个节点的,这个节点就是唯一的虚拟头节点
        dummyHead = new Node(null, null);
        size = 0;
    }

    // 获取链表中元素的个数
    public int getSize() {
        return size;
    }

    // 返回链表是否为空
    public boolean isEmpty() {
        // size是否为0
        return size == 0;
    }

    // 使用虚拟头节点添加元素
    public void add(int index, E e) {
        // 需要先判断index的合法性
        if (index < 0 || index > size) {
            throw new IllegalArgumentException("Add failed.Illegal index.");
        }

        // 此时dummyHead指向的是0元素之前一个的位置的节点
        Node prev = dummyHead;
        // 只需要遍历到index就可以了,因为是从dummyHead开始遍历的
        for (int i = 0; i < index; i++) {
            // 当前prev存的节点的下一个节点放进prev中
            // 遍历一直挪动prev位置存放下一个节点
            prev = prev.next;
        }

        prev.next = new Node(e, prev.next);
        // 插入之后维护size
        size++;
    }

    // 在链表头添加新的元素
    public void addFirst(E e) {
        add(0, e);
    }

    // 在链表的末尾添加一个新的元素e
    public void addLast(E e) {
        // 复用add(),只需要在size添加即可
        add(size, e);
    }

    // 查询操作
    public E get(int index) {
        // get之前先判断合法性
        if (index < 0 || index >= size) {
            throw new IllegalArgumentException("Add failed.Illegal index.");
        }

        // 遍历链表,是从索引为0开始的,从当前的开始遍历
        Node cur = dummyHead.next;
        for (int i = 0; i < index; i++)
            cur = cur.next;
        // 最终cur里存储的e就是需要查找的元素
        return cur.e;
    }

    // 获取链表第一个元素
    public E getFirst() {
        return get(0);
    }

    // 获取链表最后第一个元素
    public E getLast() {
        return get(size - 1);
    }

    // 链表的更新,修改
    public void set(int index, E e) {
        // 判断合法
        if (index < 0 || index >= size) {
            throw new IllegalArgumentException("Add failed.Illegal index.");
        }
        // 进行一次遍历,找到第index元素进行替换
        Node cur = dummyHead.next;
        for (int i = 0; i < index; i++)
            cur = cur.next;
        // 最终cur里存储的e等于新的e
        cur.e = e;
    }

    // 查找链表中是否存在元素e
    public boolean contains(E e) {
        // 设置cur从第一个节点开始
        Node cur = dummyHead.next;
        // 不知道循环多少次使用while
        // cur 节点不等他null的话,意味着当前cur节点是一个有效节点
        while (cur != null) {
            // cur.e是用户传来的e,返回true
            if (cur.e.equals(e))
                return true;
            // 否则就看下一个节点
            cur = cur.next;
            // 直到cur为空,说明把整个链表遍历了一遍
        }
        return false;
    }

    // 删除链表中index位置元素 返回删除的元素
    public E remove(int index) {
        // 判断合法
        if (index < 0 || index >= size) {
            throw new IllegalArgumentException("Add failed.Illegal index.");
        }

        // prev存了待删除节点之前的节点
        Node prev = dummyHead;
        for (int i = 0; i < index; i++) {
            prev = prev.next;
        }
        // 将删除的节点保存进retNode;
        Node retNode = prev.next;
        // 将当前的前一个元素的节点指向当前后一个节点
        prev.next = retNode.next;
        // 让删除元素的节点的指向为空
        retNode.next = null;
        // 维护size
        size--;
        // 将删除的元素返回
        return retNode.e;
    }

    // 删除第一个元素,返回删除的元素
    public E removeFirst() {
        return remove(0);
    }

    // 删除第一个元素,返回删除的元素
    public E removeLast() {
        return remove(size - 1);
    }

    @Override
    public String toString() {
        // 需要遍历一边整个链表中的所有的元素
        StringBuilder res = new StringBuilder();
//        Node cur = dummyHead.next;
//        while (cur != null) {
//            res.append(cur + "->");
//            cur = cur.next;
//        }
        for (Node cur = dummyHead.next; cur != null; cur = cur.next) {
            res.append(cur.e + "->");
        }
        // 最后跟一个空表示达到了链表的结尾
        res.append("NULL");
        return res.toString();
    }

}

八、测试类及结果

编写测试函数对链表类进行测试
Main.java

public class Main {
    public static void main(String[] args) {
        LinkedList<Integer> linkdelist = new LinkedList<>();
        // 为链表添加0-4这5个元素
        for (int i = 0; i < 5; i++) {
            linkdelist.addFirst(i);
            System.out.println(linkdelist);
        }

        // 测试2的位置添加100
        linkdelist.add(2,100);
        System.out.println(linkdelist);

        // 测试是否存在元素3
        boolean res = linkdelist.contains(3);
        System.out.println(res);

        // 测试删除元素
        linkdelist.remove(1);
        System.out.println(linkdelist);

        // 删除最后一个元素
        linkdelist.removeLast();
        System.out.println(linkdelist);

    }
}

测试结果:
需要注意的是:当遍历存入5个元素的时候,0在最右侧,4在最左侧

0->NULL
1->0->NULL
2->1->0->NULL
3->2->1->0->NULL
4->3->2->1->0->NULL
4->3->100->2->1->0->NULL
true
4->100->2->1->0->NULL
4->100->2->1->NULL

九、链表的时间复杂度分析

添加操作 O(n)

  • addLast(e) 向链表尾添加一个元素,O(n) 必须从链表头遍历到尾部
  • addFirst(e) 向链表头添加一个元素,O(1)
  • add(index,e) 链表任意位置添加元素,O(n/2) = O(n)

删除操作 O(n)

  • removeLast(e) 从链表尾部删除一个元素 O(n)
  • removeFirst(e) 从链表头部删除一个元素 O(1)
  • remove(index) 链表任意位置删除一个元素 O(n/2) = O(n)

修改操作 O(n)
链表不支持随机访问,需要修改元素时必须从头像后遍历查找,所以set(index,e)是O(n)

查找操作 O(n)
不论是get(index),还是contains(e),都需要从头遍历整个链表,所以是O(n)

综上所述,对于链表来说,增、删、改、查的时间复杂度都是O(n)级别的,其中对于链表头的操作都是O(1)级别的。


写在最后

如果代码有还没有看懂的或者我写错的地方,欢迎评论,我们一起学习讨论,共同进步。
推荐学习地址:
liuyubobobo老师的《玩转数据结构》:https://coding.imooc.com/class/207.html
最后,祝自己早日咸鱼翻身,拿到心仪的Offer,冲呀!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章