一. ArrayList的缺陷
上个博客已经熟悉了ArrayList的使用,并且进行了简单模拟实现。通过源码知道,ArrayList底层使用数组来存储元素:
二. 链表
2.1 链表的概念及结构
1. 单向或者双向
2. 带头或者不带头
3. 循环或者非循环
2.2 链表的实现
2.2.1 先创建一个LinkedList链表
一般的都是通过一个内部类来实现链表的创建的,显得更加的整齐。
自己写一个接口来实现存放这些方法。
这是实例化来创建了内容对象,通过next来实现链表的连接。head是头。
就是通过如图形式连接的。
2.2.2 display方法
通过创建一个singerLinkedlist对象来指向head,也就是第一个元素,通过while语句让origal指针指向空值时结束。origal=origal.next,是用来移动指针的。
结果:
2.2.3 addFirst方法
通过cur创建一个singerLinkedlist对象,存入你输入的数据,把head的地址放在cur.next当中,如下图:
然后再改变一下head指针,让它指向增加之后的第一个元素。
2.2.4 addLast方法
先判断一下,指针指向的内容是否为null,如果为null说明链表中不存在元素,第一个也就是最后一个,所以直接创建一个对象,让head指针指向它们。
如果不为空,先创建一个对象来存储你要增加的值,然后再创建一个指针也指向head指针指向的内容。通过while语句先指向最后一个位置,然后让最后一个位置的next原来为空现在把你增加的数据的地址。
只是执行完while语句的效果,然后cur1.next=cur,就是下图的效果。
2.2.5addIndex方法
在指定的位置插入你要插入的元素。
fushuyichang是我们自己写的一个异常。
如图,如果你输入一个不合法的数,就会报出输入异常的提醒。如果你要在首尾位置插入,就直接调用我们原来写的方法。
下面是我们从第一个位置到达倒数第二个位置插入的方法,先通过一个len方法来记录下第二个元素的下标,通过while语句来找到我们要插入的位置。然后通过一个temp来存下我们原来的位置的next,然后把你添加元素的地址放到当前位置的next当中,让后再把原来位置的next放到你添加的这个元素的next当中。
或者通过下面这个方式:
这样也可以成功的插入元素,不用引入第三个变量。
2.2.6 contains方法
不要在意len没有什么用,我忘记删除了,这个很简单能看懂,就是通过cur=cur.next的方式来移动指针,通过cur.val的值与key的值进行比较来判断是否找到该值,没有找到就返回false。
2.2.7 remove方法
第一种
通过双指针,一个就按照正常的顺序往后走,一个是指向它的前一个位置,当找到了该元素后,通过prev是指向前一个元素的,当cur指针找到咱要删除的元素,通过prev.next=cur.next的方式删除掉它,很巧妙,当你删除的是第一个元素时,进去prev.next就是空值了,因为prev谁也没指向,它就是一个空指针,然后直接改变head也就是头元素就删除了,删除后面的就是prev永远就不会为null了,然后通过prev.next=cur.next的方式删除了。
prev.next=cur.next的过程如下图:
原来的
当我们要删除18时,如图所示,cur找到了18,cur找到了该值,prev指向它的前一个位置,然后进行我们的操作。
执行完之后的。
这样就直接把0x77就跳过去了。
第二种
先找到你要删除的前一个,改变你要删除的前一个的next,把你要删除那个元素的下一个元素的地址放进去,也可以达到删除的效果,和上面的一样,就是它定义了两个变量我们这里定义了一个。
大家思考一下这个是否可以达到删除的效果?
答案是不能的,你的prev看似是存放着cur.next,看似和上面的一样,但是这是指针,你这相当于把prev指针指向了cur.next那个地址的值,并不是改变了,你的cur的next的值,下面的表示的是有改变了prev的指向的地址,并没有断开连接。
两种方法的区别:
第一段代码中,当满足 key - 1 == len
的条件时,执行的删除操作是 prev = cur.next.next;
,这种写法存在错误,因为它只是将 prev
指向了 cur.next.next
,但并没有将 cur.next
与链表中的后续节点正确断开连接,无法实现删除节点的效果。
第二段代码中,当满足条件时,执行的删除操作是 cur.next = cur.next.next;
,这是正确的删除节点操作,将当前节点(cur
)的下一个节点(cur.next
)指向其下下个节点(cur.next.next
),从而实现了删除 cur.next
节点的效果。
这里的cur.next直接使用和赋值给prev到底有什么区别
当将 cur.next
赋值给 prev
时(如第一段代码):
singerLinkedlist prev = cur.next;
prev = cur.next.next;
这里只是改变了 prev
的引用,让它指向了 cur.next.next
,但并没有改变链表的结构。原来 cur.next
所指向的节点仍然在链表中,没有被真正删除或断开连接。
而直接对 cur.next
进行赋值(如第二段代码):
cur.next = cur.next.next;
这直接修改了链表中当前节点(cur
)的下一个节点的引用,将其指向了下下个节点,从而实现了删除当前节点的下一个节点(cur.next
)的效果,改变了链表的结构。
总的来说,第一段代码中的操作没有对链表进行有效的删除节点操作,而第二段代码中的操作正确地实现了删除节点。
第三种
这是通过输入你要删除的值来删除的,不是通过下标。
2.2.8 removeAllKey方法
通过双指针,先把除了第一项的其他符合要求的项数全部删除。最后再判断一下第一项是否符合删除的条件。
2.2.9 size方法
2.2.10 clear方法
这种是破坏了链表结构打印的,通过cur指向head,然后cur1指向cur,通过cur=cur.next的形式来使指针向后移动,通过cur1.next=null的形式使表的结构断开,只剩下第一个了,让head=null就行了。
原来的链表结构。
while执行后的
就可以了。
或者
咱是通过head指针打印的,直接让head指针变为0就可以了。
2.3 面试题
2.3.1 删除链表中等于给定值 val 的所有节点。
要求就是
答案
通过两个指针的形式,如果一直相等,就让prev一直指向第一个元素的位置不懂,直到最后一个元素的next为null赋值给prev,此时就剩一个头head了,如果中间有不相等的,就让prev指向那个不相等的,把他保留下来。
源代码:
public ListNode removeElements(ListNode head, int val) {
if(head==null){
return head;
}
ListNode cur=head.next;
ListNode cur1=head;
while(cur!=null){
if(cur.val==val){
cur1.next=cur.next;
cur=cur.next;
}
else{
cur1=cur;
cur=cur.next;
}
}
if(head.val==val){
head=head.next;
}
return head;
}
2.3.2 翻转链表
cur和cur1的作用都是记录原来它的next的值,防止原来的next发生变化,你再让cur=cur.next,他会陷入无限循环。
源代码:
public ListNode reverseList(ListNode head) {
if(head==null){
return head;
}
ListNode cur1=head.next;
head.next=null;
while(cur1!=null){
ListNode prev=cur1.next;
cur1.next=head;
head=cur1;
cur1=prev;
}
return head;
}
2.3.3 链表的中间结点
要求:一次遍历就要完成,不能用len来记录次数。
此时我们就要定义两个引用了。
一个fast引用,走得快,slow引用走的慢,路程是一样的,fast是slow的二倍,所以fast到终点的时候,slow才走到中间位置。
为什么要有两个限制条件呢?
第二个限制条件是,当有5个数的时候,要打印后三个,fast到第三个时,fast到第二个了,判断还是符合,fast走到第五个位置了,此时slow走到第三个位置,此时就要打印了,如果只有第一个条件的话,此时还要向后走,slow就会走到第四个位置了,导致打印结果出错。
第一个限制条件的作用,光有第二个限制条件也是不行的,也不能交换位置,例如,当有四个元素时,fast走到第三个位置,slow到了第二个位置,fast走到第五个位置,此时为null,你要是直接用fast.next此时fast为空,无法访问它的next会报空指针异常。
源代码:
public ListNode middleNode(ListNode head) {
if(head==null){
return head;
}
ListNode fast=head;
ListNode slow=head;
while(fast!=null&&fast.next!=null){
fast=fast.next.next;
slow=slow.next;
}
return slow;
}
2.3.4 打印倒数的节点
要求:输入一个链表,输出该链表中倒数第k个结点及其往后的节点。
要求用一个while语句遍历一次解决。
通过len控制先不遍历slow,直到我们走到合适的位置,你要打印倒数后两个的话,fast指向最后一个的时候,slow要指向倒数第二个,也就是图中的效果。
源代码:
public void findlast(int k){singerLinkedlist fast=head;singerLinkedlist slow=head;int len=1;while (fast!=null&&fast.next!=null){if(len<k){len++;fast=fast.next;continue;}fast=fast.next;slow=slow.next;}System.out.println(slow.val); }}
2.3.5 判断回文
题目要求:
答案
先把这个链表中的值加入放到数组当中,通过数组来判断是否是回文。
源代码1:
public boolean check(singerLinkedlist head){int[] arr=new int[10];singerLinkedlist cur=head;int i=0;int sum=0;while (cur!=null){arr[i]=cur.val;cur=cur.next;sum++;i++;}for (int j = 0; j < sum/2; j++) {if (arr[j]==arr[sum-1]){}else {return false;}sum--;}return true; }
解法二:
通过快慢指针,快指针指向最后,慢指针指到中间位置,此时再通过下面的while循环翻转一下后半部分的内容,如果它是奇数,slow指针和cur指针会在中间位置相遇,此时就结束循环了,如果是偶数,通过if语句结束循环,此时到中间cur.next就是slow。
源代码2:
public boolean panduan(singerLinkedlist head){singerLinkedlist fast=head;singerLinkedlist slow=head;int len=0;while (fast!=null&&fast.next!=null){fast=fast.next.next;slow=slow.next;}singerLinkedlist cur=head;singerLinkedlist cur1=slow.next;while (cur1!=null){singerLinkedlist curn=cur1.next;cur1.next=slow;slow=cur1;cur1=curn;}while (slow!=cur){if (cur.next==slow){break;}if (cur.val!=slow.val){return false;}slow=slow.next;cur=cur.next;}return true; }
2.3.6 拼接链表
答案:
先创建一个对象,它的头是-1,让tmp指向newlist,通过while判断谁的小,就把谁接在tmp的后面,最后head和head2的长度不一定一样,所以,判断二者谁不为空就把谁后面的全部拼在tmp的后面。
源代码:
public singerLinkedlist hebing(singerLinkedlist head,singerLinkedlist head2){singerLinkedlist newlist=new singerLinkedlist(-1);singerLinkedlist tmp=newlist;while (head!=null&&head2!=null){if (head.val<head2.val){tmp.next=head;head=head.next;tmp=tmp.next;}else {tmp.next=head2;head2=head2.next;tmp=tmp.next;}}if (head!=null){tmp=head;}if (head2!=null){tmp=head2;}return newlist.next; }
2.3.7 链表分割
答案:
解法一:
创建两个对象一个是放小于x的一个放大于等于x的,通过while循环把它们分别放在不同的对象当中,可能全部都大于不存在小的,此时就直接返回prev1的next因为它的第一个是-1,不能要,如果小于的,可以再判断一下bigcur是否为空,如果是空的话就直接返回prev 的next,二者都不是空值再拼接,最后一个if语句是把最后一个元素的next设置为空值,因为如果最后一个数小于x,放在了前面,此时的最后一个next中放的还是原来的next值,此时就会形成一个闭环,无限循环输出。
源代码1:
public singerLinkedlist paixu(singerLinkedlist pHead,int x){// write code heresingerLinkedlist littlecur=new singerLinkedlist(-1);singerLinkedlist bigcur=new singerLinkedlist(-1);singerLinkedlist prev=littlecur;singerLinkedlist prev1=bigcur;singerLinkedlist cur=pHead;while(cur!=null){if(cur.val<x){littlecur.next=cur;cur=cur.next;littlecur=littlecur.next;}else{bigcur.next=cur;cur=cur.next;bigcur=bigcur.next;}}if (littlecur==null){return prev1.next;}littlecur.next=prev1.next;if (littlecur!=null){bigcur.next=null;}return prev.next;}
解法二:
先创建四个空指针。
然后把两种值一种大于等于x的,一种小于x的分别用不同的指针指向它们,首先就是先判断s1是否为空,如果不为空,说明执行了else的语句,此时s1就指向了这个小于x的头元素,作用就是找头的,不为空执行上面的,让s2动,s1不动,结束之后,s2就指向了小于x的最后一个元素,同理s4也指向了最大值的最后一个元素,判断s2是否为空,为空证明没有小于x的值,此时直接返回s4,否则直接让s2的next拼上大于等于x的第一个,就是如图效果,通过最后一个if语句将最后一个元素弄为空值。
源代码2:
public singerLinkedlist paixu2(singerLinkedlist pHead,int x){singerLinkedlist s1=null;singerLinkedlist s2=null;singerLinkedlist s3=null;singerLinkedlist s4=null;singerLinkedlist cur=pHead;while (cur!=null){if(cur.val<x){if (s1!=null){s2.next=cur;cur=cur.next;s2=s2.next;}else {s1=s2=cur;cur=cur.next;}}else {if (s4!=null){s3.next=cur;cur=cur.next;s3=s3.next;}else {s3=s4=cur;cur=cur.next;}}}if (s2==null){return s3;}//访问next了就要考虑他是否为空。s2.next=s4;if (s2!=null&&s3!=null){s3.next=null;}//因为当s2为空时说明全部都大于x,此时也就不存在什么问题,当s3为空时,说明全是小于x的,也不会存在问题,但是当s2不为空的时候,当最后一个是小于x的,// 它的前面有大于x的,此时最后一个去前面了,但是此时的最后一个还是有next的此时就会陷入循环当中。return s1; }
2.3.8 相交链表
先讲一下这里的相交不是我们创痛意义上的相交。
不是有交点的意思,而是只要有交点两个链表的后面全部得相等,是Y字型相交而不是我们的X型相交。
源代码:
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
ListNode cur1=headA;
ListNode cur2=headB;
int len1=0;
int len2=0;
while(cur1!=null){
len1++;
cur1=cur1.next;
}
while(cur2!=null){
len2++;
cur2=cur2.next;
}
cur1=headA;
cur2=headB;
int len=len1-len2;
if(len<0){
cur1=headB;
cur2=headA;
len=len2-len1;
}
while(len!=0){
cur1=cur1.next;
len--;
}
while(cur1!=cur2){
cur1=cur1.next;
cur2=cur2.next;
}
if(cur1==null){
return null;
}
return cur1;
}
}
思路:
思路就是,因为后面的要全部相等,两个链表的长度不一定相等,我们得先保证两个链表后面走的长度是一样的,所以我们就先让那个长的链表先走,保证它俩的长度一样,我们让cur1指向长链表让cur2指向短链表,我们不知道谁长,可以通过len1和len2来分别记录长度,再通过差值判断谁长,让cur1指向长的,再让cur1先走差值步,此时才可以判断是否相交,我们要的是相交的那个点,通过cur1!=cur2,如果相等了,说明就找到相交点了,下面的if语句如果cur1也就是长的为空了,说明没有找到相交点,此时返回null,否则就返回cur1说明找到了相交点。
2.3.9:环形链表
答案:
还是通过快慢指针的方式,让快指针走两步,慢指针走一步,此时二者的距离一直在缩小,如果是环形的话,总是会相遇的,如果fast==slow说明相遇了,就返回true否则就返回false。
可能会有疑问,fast能不能一次走三步,slow走一步,这是不能的,例如一个两个元素呈环形的链表,它就一直会错过,如果不成环形链表,还会发生越界情况。
源代码:
public boolean hasCycle(ListNode head) {
ListNode fast=head;
ListNode slow=head;
if(head==null||head.next==null){
return false;
}
while(fast!=null&&fast.next!=null){
fast=fast.next.next;
slow=slow.next;
if(fast==slow){
return true;
}
}
return false;
}
2.3.10 环形链表 II
答案:
思路:
源代码:
public ListNode detectCycle(ListNode head) {
ListNode fast=head;
ListNode slow=head;
ListNode cur=head;
while(fast!=null&&fast.next!=null){
fast=fast.next.next;
slow=slow.next;
if(fast==slow){
break;
}
}
if(fast==null||fast.next==null){
return null;
}
while(cur!=slow){
cur=cur.next;
slow=slow.next;
}
return cur;
}
三.双向链表
3.1 什么是双向链表
双向链表(Doubly Linked List)是链表的一种。
在双向链表中,每个节点不仅包含数据和指向下一个节点的指针(称为“后继指针”),还包含指向前一个节点的指针(称为“前驱指针”)。
这使得双向链表在某些操作上比单向链表更具优势。例如,在双向链表中,可以直接从尾部向头部遍历,而单向链表只能从头部向尾部遍历。
3.2 双向链表的优点
它解决了单向链表只能访问后一个元素的缺点,加了一个prev来表示前面的元素。
如下图所示:
加了一个prev来表示前一个变量,和一个last表示指向最后一个变量,使它既可以向前访问也可以向后访问,更加的灵活。
3.3 双向链表中的方法
3.3.1 display方法
和上面的单项链表差不多一样。
源代码:
public void display() { shuangxiang cur=head; while (cur!=null){System.out.print(cur.val+" ");cur=cur.next; }System.out.println(); }
3.3.2 size方法
和单项链表的一样
源代码:
public int size() {int count=0;shuangxiang cur=head;while (cur!=null){count++;cur=cur.next;}return count; }
3.3.3 addfrist方法
先判断是否为空,为空就直接返回,然后创建两个指针分别指向cur和cur1,直接改变第一个元素的next和prev即可,再移动一下头指针head即可。
源代码:
public void addFirst(int data) {shuangxiang l6=new shuangxiang(data);if (head==null){head=l6;last=l6;return;}shuangxiang cur=head;shuangxiang cur1=last;cur.prev=l6;l6.next=cur;l6.prev=null;head=l6; }
3.3.4 addlast方法
还是先判断是否为空,不为空就对最后一个元素进行操作。
源代码:
public void addLast(int data) { shuangxiang q6=new shuangxiang(data); if (head==null){head=last=q6;return; } last.next=q6; q6.prev=last; last=q6; }
3.3.5 addIndex方法
还是先判断是否为空,然后如果他要在第一个元素处添加,就直接调用addfrist方法,如果最后处添加,就调用addlast方法,然后用len来记录index的值,然后让prev指向原来位置的元素,让cur指向原来的前面的哪个元素,然后cur3和cur2的作用分别是记录原来的next和prev的值,使他接在后面。
源代码:
public void addIndex(int index, int data) { shuangxiang l6=new shuangxiang(data); shuangxiang cur=head; shuangxiang prev=last; if (index>size()-1||index<0){throw new fushuyichang("输入异常,超出范围"); } if (index==size()-1){addLast(data);return; } if (index==0){addFirst(data);return; } int len=index; while (len!=0){prev=prev.prev;len--; } while (len+1!=index){cur=cur.next;len++; } shuangxiang cur2=cur.next; shuangxiang cur3=prev.prev; cur.next=l6; l6.next=cur2; prev.prev=l6; l6.prev=cur3; }
3.3.6 remove方法
删除元素第一次出现的位置,然后先判断首尾是否为要删除的元素,然后再找中间的元素,通过找到要删除的元素的前一个元素,然后对他进行操作,即可删除该元素。
源代码:
public void remove(int key) { shuangxiang cur=head; shuangxiang prev=last; if (prev.val==key){shuangxiang prev2=prev.prev;prev.prev=null;prev2.next=null;return; } if (head.val==key){shuangxiang cur5=cur.next;cur.next=null;cur5.prev=null;head=cur5;return; } while (cur!=null){if (cur.next.val==key){cur.next=cur.next.next;cur.next.prev=cur;break;}cur=cur.next; } }
3.3.7 removeAllkey方法
删除出现的全部元素,首先设立几个指针分别指向不同的对象,然后通过while语句先找到你要删除的前一个元素,然后对它进行操作,使其删除它,最后还要判断头元素是否相等,相等就直接让他等于后一个删除掉它。
源代码:
public void removeAllKey(int key) {shuangxiang cur=head;shuangxiang cur3=head.next;while (cur3!=null){if (cur3.val==key){cur.next=cur.next.next;cur3=cur3.next;cur.next.prev=cur;}else {cur=cur3;cur3=cur3.next;}}if (head.val==key){head=head.next;} }
2.3.8 contains方法
和单向的一样,就是找是否存在这个元素。
源代码:
public boolean contains(int key) {shuangxiang cur=head;while (cur!=null){if (cur.val==key){return true;}cur=cur.next;}return false; }
四.ArrayList和LinkedList的区别
五.结束语
感谢大家的查看,希望可以帮助到大家,做的不是太好还请见谅,其中有什么不懂的可以留言询问,我都会一一回答。 感谢大家的一键三连。