LinkedList
LinkedList
流言蜚语我全都不搭理 只管努力就好
链表part一定要注意作图辅助理解!!
冲冲冲呀!!
四、LinkedList的模拟实现(无头双向链表)
- LinkedList linkedList= new LinkedList<>();
// 底层是双向链表 而不是单链表!
- 思路:
1.首先定义一个结点类 ListNode 再声明一个头结点和尾结点
2.打印:其实方法类似于单链表打印,只要结点不为空就打印(定义一个临时变量cur)
3.是否包含以及求长度都是类似于打印的方法,都是循环进行的(条件为是否为空)
4.头插法:首先判断是否为空 然后进行处理(头结点要移向最前面)注意尾结点的修改与否–修改对应指向就ok
5.尾插法:类似头插法
6.addIndex:首先要找到下标所在的节点(写一个方法findIndex–循环实现)
思路:检查index下标合法性–使用异常抛出 获取当前index位置的节点地址 进行修改–注意修改顺序!!!
注意要单独处理0及size下标位置(头尾插),否则就会报空指针异常!
7.remove:直接找到要删除的节点-循环寻找(但是要注意:如果删除的是头结点和尾结点的情况 还要考虑一个结点的情况)
8.removeAllKey:其实就是remove没有return就行,即一直循环到结点为null!
- 代码:
- ListNode.java
// 定义一个结点类
public class ListNode {
// 首先定义一个结点类
public int val;
public ListNode prev;
public ListNode next;
public ListNode(int val) {
this.val = val;
}
}
- IndexException.java
// 定义一个异常(下标异常)
public class ListNode {
// 首先定义一个结点类
public int val;
public ListNode prev;
public ListNode next;
public ListNode(int val) {
this.val = val;
}
}
- DoubleLinkedList.java
// 实现功能模拟
// 2、无头双向链表实现
public class DoubleLinkedList {
// ListNode 类
// 声明头尾结点
public ListNode head;
public ListNode last;
// 打印
public void display() {
// 循环实现 + 结点不为null就一直循环
ListNode cur = this.head;
while(cur != null) {
System.out.print(cur.val+" ");
// 注意这里缺少一个循环条件改变的条件!!!
cur = cur.next;
}
System.out.println();
}
//得到单链表的长度:类似打印的代码
public int size() {
// 循环
ListNode cur = this.head;
int count = 0;
while (cur != null) {
count++;
// 注意这里也是缺少条件变换!!!
cur = cur.next;
}
return count;
}
//查找是否包含关键字key是否在单链表当中
public boolean contains(int key) {
// 循环
ListNode cur = this.head;
while (cur != null) {
if(cur.val == key) {
return true;
}
cur = cur.next; // 变换的条件!!
}
return false;
}
//头插法
public void addFirst(int data) {
// 首先创建结点
ListNode listNode = new ListNode(data);
// 然后判断是否为空
ListNode cur = this.head;
if(cur == null) {
this.head = listNode;
this.last = listNode;
return ;
}
// 这里的cur可以直接换成head!qie最好用head!
// 来到这里说明链表不为空
cur.prev = listNode;
listNode.next = cur;
listNode.prev = null; // 这里不要也ok:因为新创建的节点prev 和next本来就是null
// 还有一个点不能忘记:头结点的变换!!
this.head = listNode;
// 尾结点是不用变换的!!头插时只有head需要往前移
}
//尾插法:存在错误目前!!!--已经解决!(修改后面)
public void addLast(int data) {
// 类似头插法
// 首先创建结点
ListNode listNode = new ListNode(data);
// 然后判断是否为空
ListNode cur = this.head;
if(cur == null) {
this.head = listNode;
this.last = listNode;
return ;
}
// 来到这里说明链表不为空
/* cur.next = listNode;
listNode.prev = cur;
listNode.next = null;
// 还有一个点不能忘记:尾结点的变换!!
this.last = listNode;
// 头结点是不用变换的!!尾插时只有last需要往后移*/
// 修改如下:
// 为什么一定要用last而不是cur:因为cur指向的是头结点,而尾插变换在尾结点!
// 头插可以用cur是因为cur就是head,而头插变换在head!
this.last.next = listNode;
listNode.prev = this.last;
this.last = listNode; // 一定要进行这个变换!!
}
// 找到index位置的节点
public ListNode findIndex(int index) {
ListNode cur = this.head;
// 判断下标是否合法:
if(index<0 || index>size()) {
throw new IndexException(" sorry index不合法!");
}
while(index != 0) {
cur = cur.next;
index--;
}
return cur;
}
//任意位置插入,第一个数据节点为0号下标
public void addIndex(int index,int data) {
ListNode cur = this.head;
// 找到index位置的节点
ListNode indexNode = findIndex(index);
// 创建结点:
ListNode listNode = new ListNode(data);
// 判断是否为空 + 是否只有一个结点
if(cur == null) {
this.head = listNode;
this.last = listNode;
return ;
}
// 出来就说明不是空
// 判断是否只有一个结点:
if(this.head == this.last) {
// 如果插入位置是0就是头插,是size()就是尾插
if(index==0) {
addFirst(data);
}
if(index==size()) {
addLast(data);
}
} else {
indexNode.prev.next = listNode;
listNode.prev = indexNode.prev;
listNode.next = indexNode;
indexNode.prev = listNode;
}
}
//删除第一次出现关键字为key的节点--出现错误:修改了循环条件中对于头尾结点的判断!!
public void remove(int key) {
ListNode cur = this.head;
// 判断是否为空
if(cur==null) {
return ;
}
// 进行循环
while(cur!=null) {
if(cur.val==key) {
// 判断是否为头结点 or 尾结点
if(cur==this.head) { //头结点
this.head = this.head.next;
this.head.prev = null;
return;
} else if(cur==this.last) {
this.last = this.last.prev;
this.last.next = null;
return;
} else {
cur.prev.next = cur.next;
cur.next.prev = cur.prev;
return ;
}
}
// 进行结点下移的条件!!
cur = cur.next;
}
}
//删除所有值为key的节点:对只删除一个结点进行稍微修改就行--去掉return 要让链表遍历完毕!!
public void removeAllKey(int key) {
ListNode cur = this.head;
// 判断是否为空
if(cur==null) {
return ;
}
// 进行循环
while(cur!=null) {
if(cur.val==key) {
// 判断是否为头结点 or 尾结点
if(cur==this.head) { //头结点
this.head = this.head.next;
this.head.prev = null;
} else if(cur==this.last) {
this.last = this.last.prev;
this.last.next = null;
} else {
cur.prev.next = cur.next;
cur.next.prev = cur.prev;
}
}
// 进行结点下移的条件!!!
cur = cur.next;
}
}
// 进行清空
public void clear() {
/*// 暴力清空:该方法不可行!!!就算头结点清空 但是依旧会有结点在引用这个地址
this.head = null;*/
/*// 每一个都清空:前驱后继都置null--这里的话head被修改
ListNode cur = this.head;
while(cur!=this.last) {
cur = cur.next;
this.head = null;
this.head = cur;
}
this.head=null;*/
// 进行修改如下:
// 每一个都清空:前驱后继都置null + head/last 置null (不然会一直在引用)
ListNode cur = this.head;
while(cur != null) {
ListNode curNext = cur.next;
cur.next = null;
cur.prev = null;
}
// 手动置空头尾结点
this.head = null;
this.last = null;
}
}
- Test.java
// 进行测试
public class Test {
public static void main(String[] args) {
DoubleLinkedList doubleLinkedList1 = new DoubleLinkedList();
System.out.println("使用头插法创建双向循环链表:");
doubleLinkedList1.addFirst(2);
doubleLinkedList1.addFirst(8);
doubleLinkedList1.addFirst(16);
doubleLinkedList1.addFirst(28);
doubleLinkedList1.addFirst(1998);
System.out.println("进行打印:");
doubleLinkedList1.display();
DoubleLinkedList doubleLinkedList2 = new DoubleLinkedList();
System.out.println("使用尾插法创建双向循环链表:");
doubleLinkedList2.addLast(2);
doubleLinkedList2.addLast(8);
doubleLinkedList2.addLast(2);
doubleLinkedList2.addLast(2);
doubleLinkedList2.addLast(1998);
doubleLinkedList2.display();
System.out.println("求链表长度(此处针对链表2):");
System.out.println(doubleLinkedList2.size());
System.out.println("任意位置插入(从0开始):");
doubleLinkedList1.addIndex(2,52);
doubleLinkedList1.display();
System.out.println("判断是否包含:");
System.out.println(doubleLinkedList1.contains(52));
System.out.println("进行首次出现删除:");
doubleLinkedList2.remove(2);
doubleLinkedList2.display();
System.out.println("进行所有出现都删除:");
doubleLinkedList2.removeAllKey(2);
doubleLinkedList2.display();
System.out.println("进行清空:");
doubleLinkedList1.clear();
doubleLinkedList1.display();
}
}
五、LinkedList的使用
1. 什么是LinkedList
- LinkedList 的官方文档 LinkedList官方文档
- 定义:LinkedList的底层是双向链表结构,由于(reason:链表没有将元素存储在连续的空间中,元素存储在单独的节点中,然后通过引用将节点连接起来了),因此在任意位置插入或者删除元素时,不需要搬移元素,效率比较高。
- java集合框架图:
- 说明:
- LinkedList实现了List接口
- LinkedList的底层使用了双向链表
- LinkedList没有实现RandomAccess接口,因此LinkedList不支持随机访问(注意:ArrayList支持随机访问!)
- LinkedList的任意位置插入和删除元素时效率比较高,时间复杂度为O(1)
2. LinkedList的使用
1. LinkedList的构造
- 构造方法如下:
- 实例:
public static void main(String[] args) {
// 构造一个空的LinkedList
List<Integer> list1 = new LinkedList<>();
List<String> list2 = new java.util.ArrayList<>();
list2.add("JavaSE");
list2.add("JavaWeb");
list2.add("JavaEE");
// 使用ArrayList构造LinkedList
// 注意看传入的参数!!
List<String> list3 = new LinkedList<>(list2);
}
2. LinkedList的其他常用方法介绍
- 常用方法介绍:
- LinkedList的遍历
现有:LinkedList list = new LinkedList<>();
- for循环(使用get(i))
- foreach循环(for(Integer x:list))
- 迭代器:
// 正向遍历:
ListIterator<Integer> it = list.listIterator(); // 注意写法
while(it.hasNext()){ // 注意写法hasNext()
System.out.print(it.next()+ " "); // 注意写法next()
}
// 逆向遍历:
// 注意与正向的不同(有参数传递--传递长度!)
ListIterator<Integer> rit = list.listIterator(list.size());
while (rit.hasPrevious()){ // 注意是hasPrevious()
System.out.print(rit.previous() +" "); // 注意是previous()
}
- 迭代遍历注意点:
- 逆向遍历定义变量时参数要传入大小,使之来到最后一个位置;然后判断是否有前一个,有的话打印
- 然后在使用迭代器进行正向打印结束之后直接进行迭代器的逆序打印(从while循环判断开始—而没有之前的变量重新声明定义)也ok:因为在正向遍历迭代完后直接停在了最后一个位置,跟重新赋值变量效果一样,所以此时可以直接从while循环开始逆向打印
- 只要实现了Iterable接口都可以进行迭代 但是map接
口是不可以的(map是单独的)-详见图
- LinkedLIst源码中相似的方法较多,为什么要这样设计呢?
因为底层是双向链表,不仅仅是一个链表,还可以当做栈和队列使用!(功能多)。
六、ArrayList和LinkedList的区别
- 面试题
回答“区别”问题:从共同点出发进行对比:
1.内存上的存储方式:物理上和逻辑上的连续性
2.增删查改的区别:挪不挪元素(频繁进行插入删除等操作则一般建议使用LinkedList;频繁查找建议使用ArrayList:因为ArrayList给定下标后可以直接获取到结点,但是LinkedList要依次遍历–RandomAccess接口实现速记访问)
总结区别!! - 线性表的存储方式有两种:链式存储(不连续空间)和顺序存储(连续空间)
- ArrayList和LinkedList的区别可以参照如下进行回答:
THINK
- 双向循环链表功能的模拟实现
- LinkedList的基本方法
- 相关问答
- ArrayList和LinkedList的区别(存储+访问+增删查改+应用场景)