1. LeetCode203 移除链表元素
链表基础知识
- 链表在Java中的定义:
/**
* Definition for singly-linked list.
*/
public class ListNode {
int val;
ListNode next;
ListNode() {}
ListNode(int val) { this.val = val; }
ListNode(int val, ListNode next) { this.val = val; this.next = next; }
}
- 最后的程序返回,返回head,即链表的头指针即可
- 链表中移除元素的操作:更换前一个节点指针指向的下一节点,Java中的垃圾回收机制会将移除的那个元素的内存进行回收
题目
代码实现
class Solution {
public ListNode removeElements(ListNode head, int val) {
while((head != null) && (head.val == val)){
head = head.next;
}
ListNode cur = head;
while ((cur != null) && (cur.next != null)){
if (cur.next.val == val){
cur.next = cur.next.next;
}else {
cur = cur.next;
}
}
return head;
}
}
思路
- 整体思路:如果下一个节点的值等于target,则将指针指向下下个节点。前提:头结点的值不为target,所以需要对头结点提前进行处理。
- 第一个while,操作针对的情况是,如果头结点的值即为target,那么需要把当前节点扔掉,将下一个节点定义为头节点。因为要取当前节点的val及next,所以要保证当前节点不为空,否则会报空指针异常的错误。
- 用while,不用if的原因:考虑一个链表为[7,7,7,4],target值为7。如果用了if,头节点变为下一节点,依然是target值,需要去除。移除节点值为target值的这个操作是持续的,所以要用while。
- 第一个while操作完成后,保证了头节点的值不为target值,则此时可以放心地进行下一步操作:如果下一个节点的值等于target,则将指针指向下下个节点。但是因为我们返回需要返回头节点,因此头节点不可变,因此重新定义一个节点current,不断往后遍历,指向当前遍历的节点,进行后续的操作。
- 第二个while:后续要取当前节点的值和当前节点的下一节点的值,所以这两个都不能为空。如果当前节点的下一节点值为target值,则指向下下节点;如果当前节点的下一节点值不为target值,则移动current到下一个节点,继续进入到while中进行检查。
- 最后,返回头节点。
优化
在第一个while结束后,最好加一个检查head是否为空,如果为空,可以直接返回head了,就不用再进行下面的操作了。
二解:使用虚拟头节点
代码:
class Solution {
public ListNode removeElements(ListNode head, int val) {
ListNode dummy = new ListNode(-1, head);
ListNode cur = dummy;
if (head == null){
return head;
}
while ((cur != null) && (cur.next != null)){
if (cur.next.val == val){
cur.next = cur.next.next;
}else {
cur = cur.next;
}
}
return dummy.next;
}
}
思路:
在头节点前面定义一个dummy head虚拟头节点,虚拟头节点的下一节点指向原来的头节点,这样就不用对头节点的值是否为target值进行特殊考虑了。同样定义一个current指针,对所有节点一视同仁进行操作。最后返回dummy.next,而不是原来的头节点(万一原来头节点的值就是target值呢)。
遍历链表的时候要新定义一个指针来遍历!很重要!
因为要返回头节点,如果用头节点遍历的话,头节点的值都改了,还怎么返回头节点。
2. LeetCode707 设计链表
题目
代码实现
class ListNode{
int val;
ListNode next;
ListNode(){}
ListNode(int val){
this.val = val;
}
ListNode(ListNode next){
this.next = next;
}
ListNode(int val, ListNode next){
this.val = val;
this.next = next;
}
}
class MyLinkedList {
ListNode dummy;
int size;
public MyLinkedList() {
this.size = 0;
this.dummy = new ListNode(-1);
}
public int get(int index) {
if (index >= size){
return -1;
}
ListNode cur = dummy;
for (int i = 0; i < index; i++){
cur = cur.next;
}
return cur.next.val;
}
public void addAtHead(int val) {
ListNode newNode = new ListNode(val);
if (dummy.next != null) {
newNode.next = dummy.next;
}
dummy.next = newNode;
size++;
return;
}
public void addAtTail(int val) {
ListNode newNode = new ListNode(val);
ListNode cur = dummy;
for (int i = 0; i < size; i++){
cur = cur.next;
}
cur.next = newNode;
size++;
return;
}
public void addAtIndex(int index, int val) {
if (index > size){
return;
}
if (index == size){
addAtTail(val);
return;
}
ListNode cur = dummy;
ListNode newNode = new ListNode(val);
for (int i = 0; i < index; i++){
cur = cur.next;
}
newNode.next = cur.next;
cur.next = newNode;
size++;
return;
}
public void deleteAtIndex(int index) {
if (index >= size){
return;
}
ListNode cur = dummy;
for (int i = 0; i < index; i++){
cur = cur.next;
}
cur.next = cur.next.next;
size--;
return;
}
}
/**
* Your MyLinkedList object will be instantiated and called as such:
* MyLinkedList obj = new MyLinkedList();
* int param_1 = obj.get(index);
* obj.addAtHead(val);
* obj.addAtTail(val);
* obj.addAtIndex(index,val);
* obj.deleteAtIndex(index);
*/
关键点
- 此题使用了虚拟头节点,虚拟头节点统一了对一切节点的操作,很常用,也很方便。
- 上面代码中少考虑了
index < 0
的情况,后续二刷可以加上。 - 因为力扣不需要自己定义输入输出,今天摸索了好久这道题的链表的定义。最终得出结论如下:java中没有指针的概念,所以对于每个ListNode,有两个属性。其一为自身值,其二为next,而next也是ListNode类的一个实例,这样不断传承下去。ListNode在构造时,可以无参(既无自身值,又无next),也可以有参(有自身值,无next;或有自身值,有next)
- 针对本题,MyLinkedList需要定义两个属性,一个是list的size,一个是list里构造的虚拟头节点,这两个属性不仅要在class里定义,还要在class的构造函数里进行初始化。今天最纠结的一个问题,系统怎么读出来这个List的?怎么准确的知道第二个节点是头节点的?而不是把虚拟头节点当做头节点?想了半天,想明白了,应该是循环调用了
public int get(int index)
这个函数,从index=0 获取到 index=size-1。这样就可以解释也没有定义头节点,但是输出list的时候的第一个节点是真实的头节点而不是虚拟头节点了。
3. LeetCode206 反转链表
题目
代码实现
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode reverseList(ListNode head) {
if (head == null){
return head;
}
ListNode pre = null;
ListNode cur = head;
while (cur != null){
ListNode temp = cur.next;
cur.next = pre;
pre = cur;
cur = temp;
}
return pre;
}
}
思路
这道题本来也想仿照之前两道题定义一个dummy head,最后没成功。又去看了卡哥讲的视频,消化了半天,自己画了个图,才理解。
- 为什么要定义pre并初始化为null?因为链表是以null结束的,反转之后头节点的next必须是null,所以定义一个pre指针,并初始化为null。
- 整体思路:定义一个current指针用来遍历,定义一个pre指针用来遍历新的next节点们。current指针一开始指向头节点。也即双指针法。每次循环中,提前定义一个temp指针指向cur.next,然后就可以放心地改变cur.next为pre了。为了不断地向后遍历,再将pre指针移到cur所指的位置,cur指针移到temp(也即原来cur指向的next)的位置,重复上述过程。
- 只要没有User user = new User(),单纯的User user = exist,这种都是新加一个user引用,指向已有的exist对象。new的话才是先开辟一个内存存储新对象。
总结
今天前两题目都需要:遍历链表的时候要新定义一个current指针来遍历!很重要!最后一道题双指针法。也相当于要新定义一个current和pre指针指向已有节点,进行遍历。