算法通关第一村-链表青铜挑战(Java)
1 单链表的概念
1.1 链表的概念
- 首先看一下什么是链表?单向链表就像一个铁链一样,元素之间相互连接,包含多个结点,每个结点有一个指向后继元素的next指针。表中最后一个元素的next指向null。如下图:
- 思考一下:你是否了解链表的含义?思考下面两个图,是否都满足单链表的要求为什么?
- 第二个图:
- 解析:
- 上面第一个图是满足单链表要求的,因为我们说链表要求环环相扣,核心是一个结点只能有一个后继,但不代表一个结点只能有一个被指向。第一个图中,c1被a2和b3同时指向,这是没关系的。这就好比法律倡导一夫一妻,你只能爱一个人,但是可以都多个人爱你。
第二图就不满足要求了,因为c1有两个后继a5和b4。
另外在做题的时候要注意比较的是值还是结点,有时可能两个结点的值相等,但并不是同一个结点,例如下图中,有两个结点的值都是1,但并不是同一个结点。
##2. 链表的相关概念
+节点和头节点
在链表中,每个点都由值和指向下一个结点的地址组成的独立的单元,称为一个结点,有时也称为节点,含义都是一样的。
对于单链表,如果知道了第一个元素,就可以通过遍历访问整个链表,因此第一个结点最重要,一般称为头结点。
- 虚拟结点
在做题以及在工程里经常会看到虚拟结点的概念,其实就是一个结点dummyNode,其next指针指向head,也就是dummyNode.next=head。
因此,如果我们在算法里使用了虚拟结点,则要注意如果要获得head结点,或者从方法(函数)里返回的时候,则应使用dummyNode.next。
另外注意,dummyNode的val不会被使用,初始化为0或者-1等都是可以的。既然值不会使用,那虚拟结点有啥用呢?简单来说,就是为了方便我们处理首部结点,否则我们需要在代码里单独处理首部结点的问题。在链表反转里,我们会看到该方式可以大大降低解题难度。
3创建链表
public class LinkListNode {
public int val;//数据域
public LinkListNode next;//指针域
public LinkListNode(int val){
this.val = val;
}
}
链表初始化
初始链表应考虑到,头结点与后面结点的处理方法不同,可以分开处理。
public LinkListNode head = null;
public LinkListNode initLinkList(int[] arr) {//链表初始化
LinkListNode cur = null;
for(int i = 0; i< arr.length; i++){
//给链表中每个节点的数据域赋值
LinkListNode newLinkListNode = new LinkListNode(arr[i]);
//将链表通过指针域连起来
//第一个节点
if(i ==0){
head = newLinkListNode;//让节点head和cur指向第一个节点
cur = newLinkListNode;
}else {//后面的节点
cur.next = newLinkListNode;//循环,cur的指针域依次指向后面节点
cur = newLinkListNode;//cur指向下一个节点
}
}
return head;
}
链表的遍历
- 对于单链表,不管进行什么操作,一定是从头开始逐个向后访问,所以操作之后是否还能找到表头非常重要。一定要注意"狗熊掰棒子"问题,也就是只顾当前位置而将标记表头的指针丢掉了。
public void traverseLinkList(){//链表的遍历
LinkListNode cur = head;
while(cur != null){//链表节点不为空时
System.out.print(cur.val + "-> ");
cur = cur.next;
}
System.out.println("null");
}
链表节点个数
public int getLinkListLength() {//链表的的节点个数
int length = 0;
LinkListNode cur = head;
while(cur != null){//链表节点不为空时
length++;
cur = cur.next;
}
return length;
}
链表插入
链表插入,写代码前要有清晰的构思,插入到头结点之前,插入到链表中间,插入到尾节点后面,下面三个图可以作为三种情况的参考,要考虑到插入位置‘location’值的范围。后面的链表删除也同样,当两者的‘location’值是不同的。
- 单链表的插入,和数组的插入一样,过程不复杂,但是在编码时会发现处处是坑。单链表的插入操作需要要考虑三种情况:首部、中部和尾部。
(1) 在链表的表头插入
链表表头插入新结点非常简单,容易出错的是经常会忘了head需要重新指向表头。我们创建一个新结点newNode,怎么连接到原来的链表上呢?执行newNode.next=head即可。之后我们要遍历新链表就要从newNode开始一路next向下了是吧,但是我们还是习惯让head来表示,所以让head=newNode就行了,如下图: - (2)在链表中间插入
在中间位置插入,我们必须先遍历找到要插入的位置,然后将当前位置接入到前驱结点和后继结点之间,但是到了该位置之后我们却不能获得前驱结点了,也就无法将结点接入进来了。这就好比一边过河一边拆桥,结果自己也回不去了。
为此,我们要在目标结点的前一个位置停下来,也就是使用cur.next的值而不是cur的值来判断,这是链表最常用的策略。
例如下图中,如果要在7的前面插入,当cur.next=node(7)了就应该停下来,此时cur.val=15。然后需要给newNode前后接两根线,此时只能先让new.next=node(15).next(图中虚线),然后node(15).next=new,而且顺序还不能错。
想一下为什么不能颠倒顺序?
由于每个节点都只有一个next,因此执行了node(15).next=new之后,结点15和7之间的连线就自动断开了,如下图所示: - (3)在单链表的结尾插入结点
表尾插入就比较容易了,我们只要将尾结点指向新结点就行了。
//链表节点的插入(插到头节点之前,插到链表中间,插到尾结点之后)
//location->插入到第几个节点,最小值为1,element->要插入节点的数据域值
public LinkListNode insertLinkList(int location, int element){
//要插入的新节点
LinkListNode newNode = new LinkListNode(element);
LinkListNode cur = head;
int i = 1;
if(head == null){//可以认为要插入的节点就是头结点,也可以抛出不能插入的异常
return newNode;
}
//链表的节点个数
int curLinkLength = getLinkListLength();
//插入位置不合理
if(location > (curLinkLength + 1) || location < 1 ){
System.out.println("链表插入位置错误! ");
return head;
}
if(location == 1){//插入到头结点之前
newNode.next = head;
newNode.val = element;
head = newNode;
return head;
}
//插入到链表中间或尾结点之后
while(i < (location - 1)){
i++;
cur = cur.next;//找到要插入节点位置的前一个节点
}
newNode.val = element;
newNode.next = cur.next;
cur.next = newNode;
return head;
}
链表删除
-
删除同样分为在删除头部元素,删除中间元素和删除尾部元素。
(1)删除表头结点
删除表头元素还是比较简单的,一般只要执行head=head.next就行了。如下图,将head向前移动一次之后,原来的结点不可达,会被JVM回收掉。 -
(2)删除最后一个结点
删除的过程不算复杂,也是找到要删除的结点的前驱结点,这里同样要在提前一个位置判断,例如下图中删除40,其前驱结点为7。遍历的时候需要判断cur.next是否为40,如果是,则只要执行cur.next=nul即可,此时结点40变得不可达,最终会被JVM回收掉。 -
(3)删除中间结点
删除中间结点时,也会要用cur.next来比较,找到位置后,将cur.next指针的值更新为
cur.next.next就可以解决,如下图所示:
//链表节点的删除
//location->删除第几个节点,最小值为1
public LinkListNode deleteLinkList(int location){
LinkListNode cur = head;
if(head == null){
return null;
}
//链表的节点个数
int curLinkLength = getLinkListLength();
//删除位置不合理
if(location > curLinkLength || location < 1) {
System.out.println("链表删除位置不合理");
return head;
}
if(location == 1){//删除头结点
head = head.next;
return head;
}
int count = 1;
//删除链表中间节点或尾节点
while(count < (location - 1)){
count++;
cur = cur.next;
}
cur.next = cur.next.next;
return head;
}
测试
int[] arr = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
MyLinkList myLinkList = new MyLinkList();
myLinkList.head = myLinkList.initLinkList(arr);
myLinkList.traverseLinkList();
System.out.println("链表长度为:-> " + myLinkList.getLinkListLength());
myLinkList.insertLinkList(1, 996);
myLinkList.insertLinkList(12, 55);
myLinkList.insertLinkList(8, 121);
myLinkList.traverseLinkList();
System.out.println("插入后链表长度为:-> " + myLinkList.getLinkListLength());
myLinkList.deleteLinkList(13);
myLinkList.deleteLinkList(1);
myLinkList.deleteLinkList(4);
myLinkList.traverseLinkList();
System.out.println("删除后链表长度为:-> " + myLinkList.getLinkListLength());