目录
一:引入
上一课中我们介绍了顺序表,由于其底层是一段连续空间,当在ArrayList任意位置插入或者删除元素时,就需要将后序元素整体往前或者往后搬移,时间复杂度为O(n),效率比较低,因此ArrayList不适合任意位置插入和删除比较多的场景。
为了解决这一问题,java集合中又引入了LinkedList,即链表结构。
二:链表
2.1链表的概念
链表是一种物理存储结构上非连续存储的结构,数据元素的逻辑顺序是通过链表中的引用链接次序实现的。
链表容易理解,相比于顺序表,链表的每个节点多了一个元素,用于存储下一个节点的地址。这样,我们就可以通过这一地址寻找到当前节点的下一个节点,即将所有的节点都串联起来,链式访问,故名“链表”。
2.2链表的分类
链表的不同类型,可以这样描述:
通过排列组合,我们可以得到8种不同类型的链表,其中重点需要掌握的,是如下两种:
1.无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。同时也多见于面试题中。
2.无头双向链表:在Java的集合框架库中,LinkedList底层实现就是无头双向循环链表,足见其重要性,非同小可。
三:单向链表的实现
3.1代码
在实现单链表过程中,创建了如上图所示的两个类,MySingleList class,里面实现了所有对链表所进行的操作;Test class,用于对代码逻辑进行测试。具体代码如下:
MySingleList.java
package MySingleList;
import java.util.List;
public class MySingleList {
static class ListNode {
public int val;//数值域
public ListNode next;//存储下一个节点的地址
public ListNode(int val) {
this.val = val;
}
}
public ListNode head;//代表单链表头结点的引用
public void display() {
ListNode cur = this.head;
if (this.head == null) {
System.out.println("链表为空!");
return;
}
while (cur.next != null) {
System.out.print(cur.val+" ");
cur = cur.next;
}
System.out.print(cur.val);
System.out.println();
}
//头插法
public void addFirst(int data) {
ListNode node = new ListNode(data);
node.next = head;
head = node;
}
//尾插法
public void addLast(int data) {
ListNode node = new ListNode(data);
ListNode cur = this.head;
while (cur.next != null) {
cur = cur.next;
}
cur.next = node;
}
//任意位置插入
public void addIndex(int index, int data) {
ListNode node = new ListNode(data);
if(index == 0){
addFirst(data);
return;
}
if (checkIndexAdd(index) == true) {
ListNode cur = findIndexSubOne(index);
node.next = cur.next;
cur.next = node;
}
}
private ListNode findIndexSubOne(int index) {
ListNode cur = this.head;
while((index-1) != 0){
cur = cur.next;
index--;
}
return cur;
}
private boolean checkIndexAdd(int index){
if(index < 0||index > size()){
System.out.println("你个老六,下标都不对!");
return false;
}
return true;
}
//查找关键字key是否在单链表当中
public boolean contains(int key){
ListNode cur = this.head;
while(cur.next != null){
if(cur.val == key){
return true;
}
cur = cur.next;
}
if(cur.val == key){
return true;
}
return false;
}
//删除第一次出现关键字key的节点
public void remove(int key){
ListNode cur = this.head;
if(cur.val == key){
head = cur.next;
return;
}
while(cur.next != null){
if(cur.next.val == key){
cur.next = cur.next.next;
return;
}
cur = cur.next;
}
System.out.println("没这值!");
}
//删除所有值为key的节点
public void removeAllkey(int key){
if(head == null){
return;
}
ListNode cur = head;
ListNode curNext = cur.next;
while(curNext != null){
if(curNext.val == key){
cur.next = curNext.next;
curNext = curNext.next;
}else{
cur = cur.next;
curNext = curNext.next;
}
}
if(head.val == key) {
head = head.next;
}
}
//得到单链表的长度
public int size(){
ListNode cur = this.head;
int count = 1;
while(cur.next != null){
cur = cur.next;
count++;
}
return count;
}
//清空单链表
public void clear(){
this.head = null;
}
}
Test.java
package MySingleList;
public class Test{
public static void main(String[] args){
MySingleList mySingleList = new MySingleList();
mySingleList.addFirst(1);
mySingleList.addFirst(1);
mySingleList.addFirst(1);
mySingleList.addFirst(5);
mySingleList.addFirst(1);
mySingleList.addFirst(1);
//mySingleList.addIndex(0,88);
// if(mySingleList.contains(0) == true){
// System.out.println("存在该值");
// }
mySingleList.removeAllkey(1);
//mySingleList.clear();
mySingleList.display();
}
}
3.2具体分析及部分操作详解
3.2.1头插法
//头插法
public void addFirst(int data) {
ListNode node = new ListNode(data);
node.next = head;
head = node;
}
无论原先链表中是否有元素,进行头插法的方式都是这样朴实无华,只需将待插入节点的next指向当前链表的head,然后将该节点赋值给head即可。图解如下:
3.2.2尾插法
//尾插法
public void addLast(int data) {
ListNode node = new ListNode(data);
ListNode cur = this.head;
while (cur.next != null) {
cur = cur.next;
}
cur.next = node;
}
讲解尾插法,是因为这里使用了一个cur结点。当我们需要进行尾插时,必须找到最后一个结点。为了不改变头节点head的位置,我们创建一个新的结点cur指向head,让cur移动,寻找最后一个结点,并完成插入。这种思想将会不断运用于我们之后的代码中。
3.3.3删除所有值为key的节点
//删除所有值为key的节点
public void removeAllkey(int key){
if(head == null){
return;
}
ListNode cur = head;
ListNode curNext = cur.next;
while(curNext != null){
if(curNext.val == key){
cur.next = curNext.next;
curNext = curNext.next;
}else{
cur = cur.next;
curNext = curNext.next;
}
}
if(head.val == key) {
head = head.next;
}
}
要删除所有值为key的结点,我们首先考虑这个待删除结点在链表的中间。
如果我们要删除值为23的结点,我们需要找到该结点的前一个结点,并改变其指向,让它指向待删除结点的下一个结点。所以这里定义了两个结点,一个cur,一个curNext。
如图所示,其实只需要第一条代码即可将该节点删除,而第二条代码的作用是使curNext后移,便于继续进行“排查”,以期删除所有值为key的结点。
显然当循环结束时,我们已经可以搞定除头结点外的全部结点了。因为curNext是从第二个节点开始判断的,所以头节点被忽略了。此时,我们只需要对当前头节点中的元素值进行判断即可,如果是值为key的结点,就执行head=head.next,将头结点后移一个即可。
在Test.java文件中对这部分逻辑进行测试,结果如下:
package MySingleList;
public class Test{
public static void main(String[] args){
MySingleList mySingleList = new MySingleList();
mySingleList.addFirst(1);
mySingleList.addFirst(1);
mySingleList.addFirst(23);
mySingleList.addFirst(1);
mySingleList.addFirst(24);
mySingleList.addFirst(8);
mySingleList.removeAllkey(1);
mySingleList.display();
}
}
运行结果如下:
测试任务非常简单,在此就不再赘述了。
四:LinkedList的模拟实现
LinkedList底层就是一个双向链表,我们来实现一个双向链表。
双向链表图示:
4.1代码
MyLinkedList.java
public class MyLinkedList {
static class ListNode {
public int val;
public ListNode prev;//前驱
public ListNode next;//后继
public ListNode(int val) {
this.val = val;
}
}
public ListNode head;//标记头
public ListNode last;//标记尾巴
public void display(){
ListNode cur = head;
while(cur != null){
System.out.print(cur.val+" ");
cur = cur.next;
}
System.out.println();
}
//判断下标位置合法性
public boolean checkIndex(int index){
if(index <0 | index > size()){
System.out.println("下标位置不合法!");
return false;
}
return true;
}
//头插法
public void addFirst(int data){
ListNode cur = new ListNode(data);
if(!(size()==0)){
cur.next = head;
head.prev = cur;
head = cur;
}else{
head = cur;
last = cur;
}
}
//尾插法
public void addLast(int data){
ListNode cur = new ListNode(data);
if(!(size()==0)){
last.next = cur;
cur.prev = last;
last = cur;
}else{
head = cur;
last = cur;
}
}
//任意位置插入,第一个数据节点为0号下标
public void addIndex(int index,int data){
if(!checkIndex(index)){
return;
}
ListNode cur = new ListNode(data);
if(index == 0){
addFirst(data);
return;
}
if(index == size()){
addLast(data);
return;
}
//在中间插入,需要改变四个指向
ListNode curprev = head;
while((index-1) != 0){
curprev = curprev.next;
index--;
}
//此时curprev已经指向待插入位置的前一个结点
cur.next = curprev.next;
curprev.next.prev = cur;
curprev.next = cur;
cur.prev = curprev;
}
//查找是否包含关键字key是否在双向链表当中
public boolean contains(int key){
ListNode cur = head;
while(cur.next != null){
if(cur.val == key){
return true;
}
cur = cur.next;
}
if(cur.val == key) return true;
return false;
}
//删除第一次出现关键字为key的节点
public void remove(int key){
if(head.val == key){
head = head.next;
return; //删完走人
}
ListNode cur = head;
ListNode curNext = cur.next;
while(curNext != null){
if(curNext.val == key){
cur.next = curNext.next;
if(curNext.next != null){
curNext.next.prev = cur;
return; //删完走人
}
}
cur = cur.next;
curNext = curNext.next;
}
}
//删除所有值为key的节点
public void removeAllKey(int key){
//先判断头节点
if(head.val == key){
head = head.next;
}
ListNode cur = head;
ListNode curNext = cur.next;
while(curNext != null){
if(curNext.val == key){
cur.next = curNext.next;
if(curNext.next != null){
curNext.next.prev = cur;
}
curNext = cur.next;
}else{
cur = cur.next;
curNext = curNext.next;
}
}
}
//得到单链表的长度
public int size(){
int count = 1;
ListNode cur = head;
if(cur == null){
return 0;
}
while(cur.next != null){
cur = cur.next;
count++;
}
return count;
}
public void clear(){
ListNode cur = head;
while(cur != null){
ListNode curNext = cur.next;
cur.prev = null;
cur.next = null;
cur = curNext;
}
head = null;
last = null;
}
}
Test.java
package MyLinkedList;
public class Test {
public static void main(String[] args) {
MyLinkedList linkedList = new MyLinkedList();
linkedList.addLast(12);
linkedList.addLast(23);
linkedList.addLast(34);
linkedList.addLast(45);
linkedList.addLast(56);
linkedList.display();
System.out.println("==========");
linkedList.removeAllKey(12);
linkedList.display();
System.out.println("==========");
linkedList.clear();
linkedList.display();
}
}
4.2具体分析及部分操作详解
4.2.1头插法
//头插法
public void addFirst(int data){
ListNode cur = new ListNode(data);
if(!(size()==0)){
cur.next = head;
head.prev = cur;
head = cur;
}else{
head = cur;
last = cur;
}
}
在进行头插法时,需要考虑当前链表是否为空。若当前链表为空,那么head和last结点将指向同一个位置;否则,我们需要同时考虑next与prev的指向,最后将head结点放在链表的开头。尾插法、在任意位置插入结点,其实都遵循相同的思路,其核心就是next和prev的指向问题。
4.2.2删除第一次出现关键字为key的结点
//删除第一次出现关键字为key的节点
public void remove(int key){
if(head.val == key){
head = head.next;
return; //删完走人
}
ListNode cur = head;
ListNode curNext = cur.next;
while(curNext != null){
if(curNext.val == key){
cur.next = curNext.next;
if(curNext.next != null){
curNext.next.prev = cur;
return; //删完走人
}
}
cur = cur.next;
curNext = curNext.next;
}
}
思路:
1.当头结点中数值就是key时,我们只需将头节点向后移动一位,即head=head.next,值得注意的是在移动结束后,一定要return,因为我们只需要删除第一次出现关键字为ke的结点,此时return,可以避免误删更多的结点。
2.考虑中间结点的删除。假设curNext结点是我们需要删除的结点,那么我们必须找到它的前一个结点cur,所以这里我们用到两个节点。如果curNext结点的关键字不是key,就让cur和curNext结点分别向后移动一位;当curNext结点的关键字为key时,改变相关结点的指向即可。当成功删除后,也要进行return。
测试逻辑在此不做赘述。
五:LinkedList的使用(重点)
5.1LinkedList简介
LinkedList的底层是双向链表结构,由于链表没有将元素存储在连续的空间中,元素存储在单独的节点中,然后通过引用将节点连接起来了,因此在任意位置插入或者删除元素时,不需要搬移元素,效率比较高。
【说明】
1. LinkedList实现了List接口;
2. LinkedList的底层使用了双向链表;
3. LinkedList没有实现RandomAccess接口,因此LinkedList不支持随机访问
5.2LinkedList使用
5.2.1LinkedList构造
具体示例如下:
public class TestDemo1 {
public static void main(String[] args) {
List<Integer> list1 = new LinkedList<>();
List<Integer> list2 = new LinkedList<>(list1);
}
}
5.2.2LinkedList的常用方法
具体实例如下:
package LinkedList;
import java.util.LinkedList;
import java.util.List;
public class TestDemo1 {
public static void main(String[] args) {
List<Integer> list = new LinkedList<>();
list.add(1); // add(elem): 表示尾插
list.add(2);
list.add(3);
list.add(4);
list.add(5);
list.add(6);
list.add(7);
System.out.println(list.size());
System.out.println("=========");
// 在起始位置插入0
list.add(0, 0); // add(index, elem): 在index位置插入元素elem
System.out.println(list);
if(list.contains(1)){
System.out.println("存在该元素!");
}
System.out.println("=========");
List<Integer> copy = list.subList(0,3);
System.out.println(copy);
}
}
运行结果如下:
5.3LinkedList遍历
我会介绍这些方法:直接sout输出,fori循环,foreach循环,以及使用迭代器。话不多说,直接上代码:
public static void main(String[] args) {
LinkedList<Integer> list = new LinkedList<>();
list.add(1); // add(elem): 表示尾插
list.add(2);
list.add(3);
list.add(4);
list.add(5);
list.add(6);
list.add(7);
//1.sout输出
System.out.println(list);
System.out.println();
//2.fori遍历
for (int i = 0; i < list.size(); i++) {
System.out.print(list.get(i)+" ");
}
System.out.println();
//3.foreach遍历
for (int e:list) {
System.out.print(e + " ");
}
System.out.println();
// 4.使用迭代器遍历---正向遍历
ListIterator<Integer> it = list.listIterator();
while(it.hasNext()){
System.out.print(it.next()+ " ");
}
System.out.println();
//5.使用反向迭代器---反向遍历
ListIterator<Integer> rit = list.listIterator(list.size());
while (rit.hasPrevious()){
System.out.print(rit.previous() +" ");
}
System.out.println();
}
运行结果如下:
六:ArrayList和LinkedList的区别
面试题 : 谈谈ArrayList和LinkedList的区别 ?
1.ArrayList是顺序表 , LinkedList是链表 ;
2.ArrayList支持随机访问 , 比如我要找顺序表中下标为5的元素 , 直接就是array[5] ; LinkedList显然没这功能 .
3.查找元素get : ArrayList查找元素的时间复杂度为O(1) , LinkedList因为有遍历操作 , 所以其查找元素的时间复杂度为O(N) .
4.尾插法 : 二者的时间复杂度都是O(1) .
5.任意位置插入 : ArrayList的时间复杂度是O(N) , 因为插入位置后面的所有元素 , 都要向后移位 ; 理论上 , 链表插入元素的时间复杂应该是O(1) , 因为只需要改变节点的前后指向即可 . 但是java标准库在设计LinkedList时出现了问题 , 导致没有发挥出链表的优势 . 在进行add(int index,E element)时 , 需要先遍历找到待插入的位置 , 然后将新节点插入进去 . 由于遍历找位置和插入是一个整体 , 这就导致LinkedList在插入元素时 , 时间复杂度是O(N) .
6.删除元素 : ArrayList删除元素 , 删除位置后面的所有元素 , 都要向前移位 , 所以时间复杂度为O(N) ; LinkedList删除元素 , 无论是按照下标删除还是按照值删除,都需要先遍历链表找到要删除的节点 , 所以其时间复杂度也是O(N) .
7.综上所述 , ArrayList的优势是支持随机访问 , 且查找元素比较快 ; 而有人一整谈什么 , 如果需要频繁在任意位置插入和删除元素,就使用链表 , 这是不对的 , 使用链表插入/删除高效 , 但使用LinkedList插入/删除 , 不高效 ! 因为有一个遍历的操作 !
到此为止,有关链表的知识,我已介绍完毕。所谓“纸上得来终觉浅,绝知此事要躬行”。下一篇将会更新链表相关的面试题,帮助我们更好地掌握这部分内容!
本课内容结束!