一:链表
(1)概念
链表是一种物理存储结构上非连续存储结构,数据元素的逻辑顺序是通过链表中的引用链接次序实现的
💛(链表属于常见线性表的一种)
(2)组成
由节点组成
💙(链表中节点与节点之间通过存储的地址来操作数据)
链表的存储在逻辑上是连续的,在物理上是不连续的
💜(顺序表在逻辑上和物理上都是连续存储的)
(3)节点
①一个节点有三个域,分别是val、prev、next
💓val:存数据
💓prev:存储上一个节点的地址
💓next:存储下一个节点的地址
②一个节点至少有两个域,即val和next
💚(对于双向链表,就必须得带有三个域)
③通过head引用来记录头节点的地址💚(只要知道了头节点head,接下来每个节点都能知道,可以想象成火车头)
④默认来说,最后一个节点的next无地址,用null代替
⑤默认来说,第一个节点的prev无地址,用null代替
⑥每个节点实际是个对象,带有地址的对象
⑦每个节点都是单独存在的,且地址不连续
(4)链表的分类
1.单向与双向
①单向:只能朝着一个方向,根据next存储的位置前进
②双向:既能通过prev往前一个节点走,也能通过next往后一个节点走
2.带头与不带头
①带头:头节点head不可变,插入数据不能改变头节点的位置
⭐简单理解就是专门有个头节点head放在最开始
⭐head此时就像个“哨兵”
⭐val=1所在的节点才叫做链表的首节点
②不带头:头节点head可变,如果插入数据此时头节点就变成了新节点
⭐头节点也是首节点
3.循环与非循环
①循环:尾节点的next有地址
②非循环:尾节点的next为null
4.总结类型大全
🌟八种类型的链表
1.单向带头循环
2.单向带头非循环
3.单向不带头循环
4.单向不带头非循环(⇦重点常考,大部分题型)
5.双向带头循环
6.双向带头非循环
7.双向不带头循环
8.双向不带头非循环(⇦LinkedList的底层实现)
(5)链表的优缺点
1.优点
①插入数据效率高,直接new对象,然后修改指向即可
②删除数据效率高,只需要修改指向即可
💖(不用像顺序表那样还要移动、覆盖,满了还得扩容)
2.缺点
查找数据效率低,需要遍历链表,极端情况下如果查找的数据为尾节点,要遍历整个链表
💔(无法像顺序表一样通过下标查找)
3.总结
💗链表更适合去插入数据或者删除数据
(至于查找数据和更新数据,更推荐使用ArrayList)
(6)单链表的实现(模拟)
💗模拟实现单向不带头非循环链表
import java.util.Stack;
public class MySingleList {
//定义一个内部类ListNode,表示一个节点
static class ListNode {
public int val;//值
public ListNode next;//存储下一个节点
//构造方法只能初始化val值,因为下一个节点地址未知
public ListNode(int val) {
this.val = val;
}
}
//定义一个head属性
//头节点是属于链表属性,而非节点的属性
//因此将head定义在ListNode内部类外面
public ListNode head;
//(1)创建一个链表
//很low的创建方法,但是能更好的去理解链表
//学到头插法、尾插法之后就用它们创建链表
public void createList() {
ListNode node1 = new ListNode(12);
ListNode node2 = new ListNode(23);
ListNode node3 = new ListNode(34);
ListNode node4 = new ListNode(45);
ListNode node5 = new ListNode(56);
node1.next = node2;
node2.next = node3;
node3.next = node4;
node4.next = node5;
this.head = node1;
}
//(2)遍历单链表
//无参
public void show() {
//这里不是定义了一个节点 这里只是一个引用
//将cur指向head指向的对象,不改变head,而是交给cur去操作
//如果使用head操作,等到后面head就为空了,就回不来了
ListNode cur = head;
//不可以写成cur.next!=null
//因为最后一个节点你还得打印它的val值
while (cur != null) {
System.out.print(cur.val+" ");
cur = cur.next;
}
System.out.println();
}
//(2)遍历单链表
//有参,给出指定节点开始打印
public void show(ListNode newHead) {
//这里不是定义了一个节点 这里只是一个引用
//将cur指向head指向的对象,不改变head,而是交给cur去操作
//如果使用head操作,等到后面head就为空了,就回不来了
ListNode cur = newHead;
//不可以写成cur.next!=null
//因为最后一个节点你还得打印它的val值
while (cur != null) {
System.out.print(cur.val+" ");
cur = cur.next;
}
System.out.println();
}
//(3)得到单链表的长度,就是计算链表中节点的个数
public int size(){
int count = 0;
ListNode cur = head;
while (cur != null) {
count++;
cur = cur.next;
}
return count;
}
//(4)查找是否包含关键字key在单链表当中,即找出指定的val值
public boolean contains(int key){
ListNode cur = head;
while (cur != null) {
//如果val值是引用类型
//那么这里得用equals来进行比较!
if(cur.val == key) {
return true;
}
cur = cur.next;
}
return false;
}
//(5)头插法,每次都往最前面插入
//这个时候打印的话是倒着打印的,因为每次都是头插
//例如add(1),add(2),add(3),打印就是3 2 1
//1.需要让新插入的节点其next存储之前的首节点
//2.然后head指向新插入的节点
//顺序先1后2,不可乱
//这个时候即使一个节点都没有也没关系,一样可以创建
public void addFirst(int data){
ListNode node = new ListNode(data);
node.next = head;
head = node;
}
//(6)尾插法,每次都往最后面插入
//关键在于如何找到最后一个节点,即尾节点
public void addLast(int data){
ListNode node = new ListNode(data);
//这里需要判断如果插入时一个节点都没有的清空
//此时第一个插入的节点就是我的头节点
if(head == null) {
head = node;
return;
}
ListNode cur = head;
//注意啊!!这里就是cur.next!=null而不是cur!=null了
//因为这样才能定位到最后一个节点
while (cur.next != null) {
cur = cur.next;
}
//此时cur指向的节点就是尾节点
//用这个尾节点存储新插入的节点即可
cur.next = node;
}
//(7)任意位置插入,第一个数据节点为0号下标
//插入新节点到index位置,只需要让cur走index-1步(cur是新插入节点的前一个节点)
//然后新节点的next存储cur的next即原本的下一个节点
//再让cur的next存储新插入的节点
public void addIndex(int index,int data){
int len = size();
//判断index位置的合法性
if(index < 0 || index > len) {
throw new IndexOutOfBounds("任意位置插入数据的时候,index位置不合法: "+index);
}
//0位置插入就相当于头插法
if(index == 0) {
addFirst(data);
return;
}
//位置刚好是节点个数长度,就尾插法
if(index == len) {
addLast(data);
return;
}
//先找到index-1位置的节点
ListNode cur = findIndex(index);
//进行插入
ListNode node = new ListNode(data);
node.next = cur.next;
cur.next = node;
}
//找到index-1位置的节点
private ListNode findIndex(int index) {
ListNode cur = head;
while (index - 1 != 0) {
cur = cur.next;
index--;
}
return cur; //此时cur就是index-1位置的节点
}
//(8)删除第一次出现关键字为key的节点
public void remove(int key){
//判断头节点为空即一个节点都没有的情况
if(head == null) {
return;
}
//如果删除的刚好是头节点
if(head.val == key) {
head = head.next;
return;
}
//得到要删除节点的前一个节点prev
ListNode prev = searchPrev(key);
//判断prev是否为null
if(prev == null) {
System.out.println("没有这个数据!");
return;
}
//删除
ListNode del = prev.next;
prev.next = del.next;
}
//找到要删除节点的前一个节点prev
private ListNode searchPrev(int key) {
ListNode prev = head;
while (prev.next != null) {
if(prev.next.val == key) {
return prev;
}else {
prev = prev.next;
}
}
return null;
}
//(9)删除所有值为key的节点(面试题:要求只遍历链表一遍)
//双指针法:cur表示要删除的节点;prev表示当前节点的前一个节点
public void removeAllKey(int key){
if(head == null) {
return;
}
ListNode cur = head.next;
ListNode prev = head;
while (cur != null) {
if(cur.val == key) {
prev.next = cur.next;
cur = cur.next;
}else {
prev = cur;
cur = cur.next;
}
}
//删完所有key(除头节点外)
//万一head.val也是要删除的值
//把头节点key放最后删除
if(head.val == key) {
head = head.next;
}
}
//(10)清空链表
public void clear() {
//方法一:直接将头节点head设置为空
//this.head = null;
//方法二:每个节点都设置为空
while (head != null) {
//headNext的作用是记录下一个节点的位置
//因为当你将一个节点设置为空后,你就找不到下一个节点的位置了
ListNode headNext = head.next;
head.next = null;
head = headNext;
}
}
}
🌟重要代码理解
①while(cur.next!=null)与while(cur!=null)
②cur=cur.next
二:链表题目
(1)删除链表中所有值为key的节点
💛题目链接:删除链表中所有值为key的节点
💓(要求只遍历链表一遍)
①思路与代码分析:
(图片很长,建议右键图像,然后“标签页打开图像”放大看)
②方法代码://双指针法:cur表示要删除的节点;prev表示当前节点的前一个节点 public void removeAllKey(int key){ //判断当前是否一个节点都没有的情况 if(head == null) { return; } ListNode cur = head.next; ListNode prev = head; while (cur != null) { if(cur.val == key) { prev.next = cur.next; cur = cur.next; }else { prev = cur; cur = cur.next; } } //删完所有key(除头节点外) //万一head.val也是要删除的值 //把头节点key放最后删除 if(head.val == key) { head = head.next; } }
③测试代码:
public class TestMySingleList { public static void main(String[] args) { MySingleList mySingleList = new MySingleList(); mySingleList.addLast(9); mySingleList.addLast(56); mySingleList.addLast(56); mySingleList.addLast(99); System.out.println("删除前:"); mySingleList.show(); mySingleList.removeAllKey(56); System.out.println("删除后:"); mySingleList.show(); } }
(2)反转链表
💛题目链接:反转链表
①思路与代码分析:
(图片很长,建议右键图像,然后“标签页打开图像”放大看)
②方法代码:
//题目:反转链表 public ListNode reverseList() { //情况:一个节点都没有 if(head == null) { return null; } //情况:只有一个节点 if(head.next == null) { return head; } //情况:有节点,真正开始翻转 //cur代表当前需要翻转的节点 ListNode cur = head.next; head.next = null; while(cur != null) { //curNext记录当前需要翻转节点的下一个节点 ListNode curNext = cur.next; cur.next = head; head = cur; cur = curNext; } return head; }
③测试代码:
public class TestMySingleList { public static void main(String[] args) { MySingleList mySingleList = new MySingleList(); mySingleList.addLast(12); mySingleList.addLast(66); mySingleList.addLast(43); mySingleList.addLast(77); System.out.println("反转前:"); mySingleList.show(); System.out.println("反转后:"); mySingleList.reverseList(); mySingleList.show(); } }
(3)链表的中间节点
💛题目链接:链表的中间节点
①思路与代码分析:
(图片很长,建议右键图像,然后“标签页打开图像”放大看)
②方法代码:
//找出中间节点 public ListNode middleNode(ListNode head) { //判断当前没有节点的情况 if(head == null) { return null; } //判断当前只有一个节点的情况 if(head.next == null) { return head; } //定义快慢指针 ListNode fast = head; ListNode slow = head; while(fast != null && fast.next != null) { fast = fast.next.next; slow = slow.next; } //slow此时就是中间节点 return slow; }
③测试代码:
public class TestMySingleList { public static void main(String[] args) { MySingleList mySingleList1 = new MySingleList(); mySingleList1.addLast(12); mySingleList1.addLast(66); mySingleList1.addLast(43); mySingleList1.addLast(77); System.out.print("偶数节点情况下的输入:"+" "); mySingleList1.show(); MySingleList.ListNode ret1 = mySingleList1.middleNode(mySingleList1.head); System.out.println("偶数节点情况下中间节点:"+ret1.val); System.out.print("偶数节点情况下的输出:"+" "); mySingleList1.show(ret1); System.out.println(""); MySingleList mySingleList2 = new MySingleList(); mySingleList2.addLast(12); mySingleList2.addLast(23); mySingleList2.addLast(34); mySingleList2.addLast(45); mySingleList2.addLast(77); System.out.print("奇数节点情况下的输入:"+" "); mySingleList2.show(); MySingleList.ListNode ret2 = mySingleList2.middleNode(mySingleList2.head); System.out.println("奇数节点情况下中间节点:"+ret2.val); System.out.print("奇数节点情况下的输出:"+" "); mySingleList1.show(ret2); } }
(4)链表中倒数第k个结点
💛题目链接:输出该链表中倒数第k个结点
①思路与代码分析:
(图片很长,建议右键图像,然后“标签页打开图像”放大看)
②方法代码:
//找出倒数第K个节点 public ListNode findKthToTail(int k) { //判断k的合法性 if(k <= 0 || head == null) { return null; } //定义快慢指针 ListNode fast = head; ListNode slow = head; //1、先让fast走k-1步 for (int i = 0; i < k-1; i++) { fast = fast.next; if(fast == null) { return null; } } //2、同时走 走到fast.next==null的时候停止! 返回slow while (fast.next != null) { fast = fast.next; slow = slow.next; } //slow此时就是我要找的倒数第k个节点 return slow; }
③测试代码:
public class TestMySingleList { public static void main(String[] args) { MySingleList mySingleList = new MySingleList(); mySingleList.addLast(12); mySingleList.addLast(23); mySingleList.addLast(34); mySingleList.addLast(45); mySingleList.addLast(77); System.out.print("输入:"+" "); mySingleList.show(); int k =4; MySingleList.ListNode ret1 = mySingleList.findKthToTail(k); System.out.println("倒数第"+k+"个节点:"+" "+ret1.val); System.out.print("输出:"+" "); mySingleList.show(ret1); } }
(5)合并两个有序链表
💛题目链接:合并两个有序链表
①思路与代码分析:
(图片很长,建议右键图像,然后“标签页打开图像”放大看)
②方法代码:
//合并有序链表 public ListNode mergeTwoLists(ListNode headA , ListNode headB){ //定义一个newHead和一个tmpH ListNode newHead = new ListNode(); ListNode tmpH = newHead; //循环合并过程 while(headA!=null && headB!=null){ if(headB.val<headA.val){ tmpH.next=headB; //tmpH指向小的值也可以写成:tmpH=headB; tmpH=tmpH.next; headB=headB.next; }else{ tmpH.next=headA; //tmpH指向小的值也可以写成:tmpH=headA; tmpH=tmpH.next; headA=headA.next; } } //假设此时headB已经遍历完了,headA没有 if(headA!=null){ tmpH.next=headA; } //假设此时headA已经遍历完了,headB没有 if (headB!=null){ tmpH.next=headB; } //此时就是一个合并且有序的新链表 return newHead.next; }
③测试代码:
public class TestMySingleList { public static void main(String[] args) { MySingleList mySingleListT = new MySingleList(); MySingleList mySingleList1 = new MySingleList(); mySingleList1.addLast(12); mySingleList1.addLast(23); mySingleList1.addLast(34); mySingleList1.addLast(45); mySingleList1.addLast(77); System.out.print("链表1:"+" "); mySingleList1.show(); MySingleList mySingleList2 = new MySingleList(); mySingleList2.addLast(9); mySingleList2.addLast(15); mySingleList2.addLast(25); mySingleList2.addLast(30); System.out.print("链表2:"+" "); mySingleList2.show(); MySingleList.ListNode ret = mySingleListT.mergeTwoLists(mySingleList1.head,mySingleList2.head); mySingleListT.show(ret); } }
(6)链表分割
💛题目链接:链表分割
①思路与代码分析:
(图片很长,建议右键图像,然后“标签页打开图像”放大看)
②方法代码:
//分割链表 public ListNode partition( int x) { // 定义两个段 ListNode bs = null; ListNode be = null; ListNode as = null; ListNode ae = null; ListNode cur = head; while (cur != null) { //小于x的情况 if(cur.val < x) { //第一次插入的时候 if(bs == null) { bs = cur; be = cur; }else { be.next = cur; //be往后走一步也可以是:be=be.next be = cur; } } //大于或等于x的情况 else { //第一次插入的时候 if(as == null) { as = cur; ae = cur; }else { ae.next = cur; //ae往后走一步也可以是:ae=ae.next ae = cur; } } cur = cur.next; } //要考虑: 难道2个段当中 都有数据吗? // 第一个段没有数据 直接返回第2个段!! // 小于x的段没有数据了,直接返回大于x的段 if(bs == null) { return as; } //将两个段进行连接 be.next = as; //如果大于x的段有节点,需要将ae.next设置为空 //因为ae肯定是作为链表的最后一个节点 if(as != null) { ae.next = null; } //作用一: 大于x的段没有数据了,直接返回小于x的段 //作用二: 返回已经分好段的链表 return bs; }
③测试代码:
public class TestMySingleList { public static void main(String[] args) { MySingleList mySingleListT = new MySingleList(); MySingleList mySingleList = new MySingleList(); mySingleList.addLast(45); mySingleList.addLast(12); mySingleList.addLast(34); mySingleList.addLast(23); mySingleList.addLast(77); System.out.print("输入链表:"+" "); mySingleList.show(); MySingleList.ListNode ret = mySingleList.partition(25); System.out.print("分割链表:"+" "); mySingleList.show(ret); } }
(7)链表的回文结构
💛题目链接:链表的回文结构
①思路与代码分析:
(图片很长,建议右键图像,然后“标签页打开图像”放大看)
②方法代码:
public boolean chkPalindrome() { //0.判断一个节点都没有的情况 if(head == null) return false; //1、找中间节点 ListNode fast = head; ListNode slow = head; while (fast != null && fast.next != null) { fast = fast.next.next; slow = slow.next; } //此时slow所指的位置就是中间节点 //2、开始翻转 ListNode cur = slow.next; while (cur != null) { ListNode curNext = cur.next; cur.next = slow; slow = cur; cur = curNext; } //此时翻转完成 //3、开始判断是否为回文 while (head != slow) { if(head.val != slow.val) { return false; } //针对偶数节点 if(head.next == slow) { return true; } head = head.next; slow = slow.next; } return true; }
③测试代码:
public class TestMySingleList { public static void main(String[] args) { MySingleList mySingleList = new MySingleList(); mySingleList.addLast(12); mySingleList.addLast(23); mySingleList.addLast(34); mySingleList.addLast(23); mySingleList.addLast(12); System.out.print("输入链表:"+" "); mySingleList.show(); boolean ret = mySingleList.chkPalindrome(); System.out.println(ret); } }
(8)两个链表,找出它们第一个公共节点
💛题目链接:输入两个链表,找出它们的第一个公共节点
①思路与代码分析:
(图片很长,建议右键图像,然后“标签页打开图像”放大看)
②方法代码:
public ListNode getIntersectionNode(ListNode headA,ListNode headB) { //判断其中有一个链表为空的情况 if(headA==null && headB!=null){ return null; } if(headA!=null && headB==null){ return null; } //假设链表A长(假设的不一定就是长的,后面需要作出调整) MySingleList.ListNode plong = headA; MySingleList.ListNode pshort = headB; //1、分别求两个链表的长度 int len1 = 0; int len2 = 0; //求链表A的长度 while (plong != null) { len1++; plong = plong.next; } //求链表B的长度 while (pshort != null) { len2++; pshort = pshort.next; } //求完长度再回到首节点 //(没有这一步,上面求完长度的Plong和Pshort就全都为null了) plong = headA; pshort = headB; //2、求差值步的len int len = len1 - len2; //假设错误的情况,就会导致len<0,需要作出调整 if(len < 0) { plong = headB; pshort = headA; len = len2 - len1; } //此时就保证了plong一定指向最长的链表;pshort一定指向最短的链表;len一定是一个正数 //3、哪个链表长就先走len步 while (len != 0) { plong = plong.next; len--; } //4、一起走;直到相遇! while (plong != pshort) { plong = plong.next; pshort = pshort.next; } return plong; }
(9)环形链表I
💛题目链接:环形链表
①思路与代码分析:
(图片很长,建议右键图像,然后“标签页打开图像”放大看)
②方法代码:
//判断链表是否形成环 public boolean hasCycle(ListNode head) { //判断一个节点都没有的情况 if(head == null) { return false; } //定义快指针fast和慢指针slow ListNode fast = head; ListNode slow = head; //有环则相遇 while(fast != null && fast.next != null) { fast = fast.next.next; slow = slow.next; if(fast == slow) { return true; } } return false; }
(10)环形链表II
💛题目链接:给定一个链表,返回链表开始入环的第一个节点给定一个链表,返回链表开始入环的第一个节点
①思路与代码分析:
(图片很长,建议右键图像,然后“标签页打开图像”放大看)
②方法代码:
public ListNode detectCycle(ListNode head) { if(head == null) return null; //定义fast和slow ListNode fast = head; ListNode slow = head; //判环 while(fast != null && fast.next != null) { fast = fast.next.next; slow = slow.next; if(fast == slow) { break; } } //说明没有环,直接返回null if(fast == null || fast.next == null) { return null; } //注:这里必须要让fast回到头节点,因为在上面判环是fast和slow处于相遇点了 //我们的目的:fast从头节点出发,slow从相遇点出发,slow就不用管了 fast = head; //fast和slow一次走一步 while(fast != slow) { fast = fast.next; slow = slow.next; } //跳出循环就代表fast==slow,此时相遇了,就返回slow即可 return slow; }
三:LinkedList
(1)LinkedList的定义
💗LinkedList是一个集合类,实现了List接口
(2)LinkedList的本质
💓LinkedList的底层是双向链表
(3)LinkedList的细节
1.LinkedList实现了List接口
2.LinkedList底层实现了双向链表
3.LinkedList没有实现RandomAccess接口,因此LinkedList不支持随机访问
4.LinkedList的任意位置插入和删除元素时效率较高
(不适合查找和修改,查找和修改最好使用ArrayList)
(4)LinkedList的创建对象
①可调用LinkedList的所有方法
LinkedList<Integer> linkedlist = new LinkedList<>();
②向上转型,List是父类,发生了动态绑定,只能调用子类重写父类List接口的方法,不能调用子类特有的方法
List<Integer> linkedlist = new LinkedList<>();
🌟实例化的时候推荐使用泛型<>;指定一下存放的数据类型,避免数据类型杂乱无章
(5)LinkedList的构造方法
①LinkedList():无参构造方法
②LinkedList(Collection<? extends E> c):利用其他 Collection构建 ArrayList
1.必须是实现了Collection接口的
2.<? extends E>:带有泛型;说明你要传入的数据,它的泛型参数必须是E或E的子类
(与之前发布的ArrayList文章相似,具体看目录四(5):顺序表与ArrayList)
(6)LinkedList的普通方法
(7)LinkedList的四种遍历
1.System输出
System.out.println( )
💛(原因在于LinkedList父类重写了toString方法)
import java.util.LinkedList; import java.util.List; public class TestLinkedList { public static void main(String[] args) { List<String> linkedlist = new LinkedList<>(); linkedlist.add("hlizoo"); linkedlist.add("777777"); System.out.println(linkedlist); } }
2.for-each循环(常用)
①冒号的右边是要遍历的集合/数组
②冒号的左边是遍历集合/数组中的类型+元素
import java.util.LinkedList; import java.util.List; public class TestLinkedList { public static void main(String[] args) { List<String> linkedlist = new LinkedList<>(); linkedlist.add("I"); linkedlist.add("like"); linkedlist.add("java"); for (String x:linkedlist) { System.out.print(x+" "); } } }
3.迭代器正向遍历
①通过调用LinkedList集合的iterator或listIterator方法获取迭代器对象
②boolean hasNext():
hasNext
方法用于检查是否还有下一个元素
③E next():
next
方法用于获取下一个元素的值
import java.util.Iterator; import java.util.LinkedList; import java.util.List; public class TestLinkedList { public static void main(String[] args) { List<String> linkedlist = new LinkedList<>(); linkedlist.add("I"); linkedlist.add("like"); linkedlist.add("java"); Iterator<String> iterator = linkedlist.listIterator(); while(iterator.hasNext()){ System.out.print(iterator.next()+" "); } System.out.println(); } }
4.迭代器反向遍历
①通过调用LinkedList集合的iterator或listIterator方法获取迭代器对象
(参数就是从指定位置开始打印)
②boolean hasPrevious():hasPrevious方法用于检查是否还有上一个元素
③E previous():
previous
方法用于获取上一个元素的值
import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.ListIterator; public class TestLinkedList { public static void main(String[] args) { List<String> linkedlist = new LinkedList<>(); linkedlist.add("I"); linkedlist.add("like"); linkedlist.add("java"); ListIterator<String> iterator = linkedlist.listIterator(linkedlist.size()); while(iterator.hasPrevious()){ System.out.print(iterator.previous()+" "); } System.out.println(); } }
(8)LinkedList的模拟实现
🌟这里实际上也就是双向链表的模拟实现
public class MyLinkedList {
//一个节点就包括了val、prev、next
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 int size(){
int len = 0;
ListNode cur = head;
while (cur != null) {
cur = cur.next;
len++;
}
return len;
}
//打印双向链表(跟单向代码一样,与是否双向无关)
public void display(){
ListNode cur = head;
while (cur != null) {
System.out.print(cur.val+" ");
cur = cur.next;
}
System.out.println();
}
//查找是否包含关键字key是否在链表当中(跟单向代码一样,与是否双向无关)
public boolean contains(int key){
ListNode cur = head;
while (cur != null) {
if(cur.val == key) {
return true;
}
cur = cur.next;
}
return false;
}
//头插法
public void addFirst(int data){
ListNode node = new ListNode(data);
if(head == null) {
head = node;
last = node;
return;
}
node.next = head;
head.prev = node;
head = node;
}
//尾插法
public void addLast(int data){
ListNode node = new ListNode(data);
if(head == null) {
head = node;
last = node;
return;
}
last.next = node;
node.prev = last;
last = node; //或者last = last.next;
}
//任意位置插入,第一个数据节点为0号下标
public void addIndex(int index,int data){
//求链表长度
int size = size();
//判断index下标合法性
if(index < 0 || index > size) {
throw new IndexOutOfBounds("双向链表index不合法!");
}
//index为0,头插法
if(index == 0) {
addFirst(data);
return;
}
//index为链表长度,尾插法
if(index == size) {
addLast(data);
return;
}
//给一个cur,目的是来到插入下标的位置
//(与单链表不同,这里双链表需要让cur走到index位置,而单链表的cur只需要走index-1步)
ListNode cur = head;
while (index != 0) {
cur = cur.next;
index--;
}
//正式插入
ListNode node = new ListNode(data);
node.next = cur;
cur.prev.next = node;
node.prev = cur.prev;
cur.prev = node;
}
//删除第一次出现关键字为key的节点
public void remove(int key){
ListNode cur = head;
while (cur != null) {
if(cur.val == key) {
//开始删
if(cur == head) {
//如果要删除的是头节点
head = head.next;
//只有1个节点的时候走else
if(head != null) {
head.prev = null;
}else {
last = null;
}
}else {
//删除尾节点
//是尾节点走else
cur.prev.next = cur.next;
if(cur.next != null) {
cur.next.prev = cur.prev;
}else {
last = last.prev;
}
}
return;
}else {
cur = cur.next;
}
}
}
//删除所有值为key的节点
public void removeAllKey(int key){
ListNode cur = head;
while (cur != null) {
if(cur.val == key) {
//开始删
if (cur == head) {
//如果要删除的是头节点
head = head.next;
//只有1个节点的时候走else
if (head != null) {
head.prev = null;
}else {
last = null;
}
} else {
//删除尾节点
//是尾节点走else
cur.prev.next = cur.next;
if (cur.next != null) {
//cur.prev.next = cur.next;
cur.next.prev = cur.prev;
} else {
//cur.prev.next = cur.next;
last = last.prev;
}
}
}
cur = cur.next;
}
}
public void clear(){
ListNode cur = head;
while (cur != null) {
ListNode curNext = cur.next;
cur.prev = null;
cur.next = null;
//cur.val = null;
cur = curNext;
}
head = null;
last = null;
}
}
(9)LinkedList与ArrayList的区别
1.存储空间
①ArrayList:底层是数组,物理上和逻辑上存储均连续
②LinkedList:底层是双向链表,逻辑上存储连续,物理上存储不连续
2.插入
①ArrayList:需要移动元素才可插入,效率低,时间复杂度O(N)
②LinkedList:只需修改指向即可插入,效率高,时间复杂度O(1)
3.查询
①ArrayList:通过下标直接查询,效率高
②LinkedList:只能通过遍历,效率低
4.插入空间不够
①ArrayList:空间不够需要扩容
②LinkedList:没有容量大小之说
5.随机访问
①ArrayList:可以
②LinkedList:不可以
6.应用场景
①ArrayList:查改
②LinkedList:增删