文章目录
一、链表的基本方法
链表是一种通过节点存储元素,并且节点与节点之间是连接起来的数据结构。
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后
- 首先将node的next指向prev的下一个元素,
node.next = prev.next;
在这里prev.next是插入位置的后一个节点
- 之后让prev的next等于node
prev.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,冲呀!