代码随想录算法训练营第三天 | 203.移除链表元素 707.设计链表 206.反转链表
第一次试着写博客,随便写写。
题目一:移除链表元素
删除链表中等于给定值 val 的所有节点。请你删除链表中所有满足 Node.val == val
的节点,并返回 新的头节点。
示例 1: 输入:head = [1,2,6,3,4,5,6], val = 6 输出:[1,2,3,4,5]
示例 2: 输入:head = [], val = 1 输出:[]
示例 3: 输入:head = [7,7,7,7], val = 7 输出:[]
本题可以创建一个哨兵节点temphead(保存值为-1),让temphead的next指向head。
由于需要实际变化过后的head,因此返回temphead.next而不是不动的head.
使用两个指针before和 p 一前一后移动,删除元素时用before和p.next跳过p指向的节点。
class Solution {
public ListNode removeElements(ListNode head, int val) {
if(head == null)
{
return head;
}
ListNode temphead = new ListNode(-1,head);
ListNode p = head;
ListNode before = temphead;
while(before.next != null)
{
if(val == p.val)
{
before.next = p.next;
}else{
before = p;
}
p = p.next;
}
return temphead.next;
}
}
注意:
c++在堆区申请内存后仍需delete手动回收,而java中有虚拟机内存回收机制,不用手动回收。
c++会使用 (*p).next 或 p->next 的形式解引用,而java是在调用对象,直接 p.next 即可。
(用java而不是c++之后,感觉以前写链表用c++被指针和引用搞的云里雾里,用java结果能理解记住了,真的奇怪)
题目二:链表实现(java,单链表)
实现过程没有什么大坑,不过有几点需要注意:
1.可以将节点和列表实现用两个类分开,有助于代码简洁;
2.数组可通过下标查找或遍历元素,java的链表遍历可直接在对象中插入index属性以计数。
同时,这也意味着,可以不用靠节点指针或者迭代器遍历链表,直接for循环就行。
3.无论是头插法还是尾插法,都可统一成相同的插入方法而使用不同的参数。可以调用另外的插入函数。
4.注意index边界和空指针问题。
(个人实现时出现了大问题,就不贴出来了,下面是标答)
//单链表
class ListNode {
int val;
ListNode next;
ListNode(){}
ListNode(int val) {
this.val=val;
}
}
class MyLinkedList {
//size存储链表元素的个数
int size;
//虚拟头结点
ListNode head;
//初始化链表
public MyLinkedList() {
size = 0;
head = new ListNode(0);
}
//获取第index个节点的数值,注意index是从0开始的,第0个节点就是头结点
public int get(int index) {
//如果index非法,返回-1
if (index < 0 || index >= size) {
return -1;
}
ListNode currentNode = head;
//包含一个虚拟头节点,所以查找第 index+1 个节点
for (int i = 0; i <= index; i++) {
currentNode = currentNode.next;
}
return currentNode.val;
}
//在链表最前面插入一个节点,等价于在第0个元素前添加
public void addAtHead(int val) {
addAtIndex(0, val);
}
//在链表的最后插入一个节点,等价于在(末尾+1)个元素前添加
public void addAtTail(int val) {
addAtIndex(size, val);
}
// 在第 index 个节点之前插入一个新节点,例如index为0,那么新插入的节点为链表的新头节点。
// 如果 index 等于链表的长度,则说明是新插入的节点为链表的尾结点
// 如果 index 大于链表的长度,则返回空
public void addAtIndex(int index, int val) {
if (index > size) {
return;
}
if (index < 0) {
index = 0;
}
size++;
//找到要插入节点的前驱
ListNode pred = head;
for (int i = 0; i < index; i++) {
pred = pred.next;
}
ListNode toAdd = new ListNode(val);
toAdd.next = pred.next;
pred.next = toAdd;
}
//删除第index个节点
public void deleteAtIndex(int index) {
if (index < 0 || index >= size) {
return;
}
size--;
if (index == 0) {
head = head.next;
return;
}
ListNode pred = head;
for (int i = 0; i < index ; i++) {
pred = pred.next;
}
pred.next = pred.next.next;
}
}
题目三:206反转链表
如果再定义一个新的链表,实现链表元素的反转,其实这是对内存空间的浪费。
其实只需要改变链表的next指针的指向,直接将链表反转 ,而不用重新定义一个新的链表.
class Solution
{
//iteration
public ListNode reverseList(ListNode head) {
ListNode pre = null;
ListNode cur = head;
ListNode temp = null;
while(cur != null)
{
temp = cur.next;//预先指向后一个节点
cur.next = pre;
pre = cur;
cur = temp;
}
return pre;
}
}
用迭代法实现反转。
注意这里实际用了三个指针实现反转:
pre:previous,指前面指针; cur:current,指当前指针;
pre和cur是双指针,让pre一上来指向空而不是head可以规避特殊情况的判定。(确实一上来让pre指向head还挺麻烦的)
temp得提前保存cur原来应该指向的下一个节点(不然cur没的指向了)。
最后返回pre,为新链表的头节点。此时cur指向空。
注意迭代法的一次迭代:每次判定cur不为空,之后进行pre和cur赋值的操作,将cur赋给pre,temp赋值给cur.这是每一次迭代要求的,也是每一层递归时应该等价实现的内容。
用递归法实现反转。
class Solution
{
//recursion
public ListNode reverseList(ListNode head) {
return reverse(null, head);
//起始条件,剩下的交给reverse递归解决
}
public ListNode reverse(ListNode pre, ListNode cur)
{
if(cur == null)//边界条件
{
return pre;
}
ListNode temp = cur.next;
cur.next = pre;
//如下递归的写法,其实就是做了这两步
// pre = cur;
// cur = temp;
reverse(cur, temp);
//理解这一步是关键
}
}
唯一的关键是reverse()嵌套调用reverse时。里面的reverse()功能对标了迭代法的每一次迭代赋值。迭代时将cur赋给pre,temp赋值给cur,实质实现的是节点交换顺序。同样,每次调用reverse时也同样在实现此功能,实现交换。因此子递归调用reverse(),形参为cur和temp,将它们实现交换(实质我们也确实在这样做)。
时间复杂度: O(n), 递归处理链表的每个节点
空间复杂度: O(n), 递归调用了 n 层栈空间
还有另外一种与双指针法不同思路的递归写法:从后往前翻转指针指向。很有意思。
// 从后向前递归
class Solution
{
ListNode reverseList(ListNode head) {
// 边缘条件判断
if(head == null)
return null;
if (head.next == null)
return head;
// 递归调用,翻转第二个节点开始往后的链表
ListNode last = reverseList(head.next);
// 翻转头节点与第二个节点的指向
head.next.next = head;
// 此时的 head 节点为尾节点,next 需要指向 NULL
head.next = null;
return last;
}
}