三、设计链表
题目:
你可以选择使用单链表或者双链表,设计并实现自己的链表。
单链表中的节点应该具备两个属性:val 和 next 。val 是当前节点的值,next 是指向下一个节点的指针/引用。
如果是双向链表,则还需要属性 prev 以指示链表中的上一个节点。假设链表中的所有节点下标从 0 开始。
实现 MyLinkedList 类:
- MyLinkedList() 初始化 MyLinkedList 对象。
- int get(int index) 获取链表中下标为 index 的节点的值。如果下标无效,则返回 -1 。
- void addAtHead(int val) 将一个值为 val 的节点插入到链表中第一个元素之前。在插入完成后,新节点会成为链表的第一个节点。
- void addAtTail(int val) 将一个值为 val 的节点追加到链表中作为链表的最后一个元素。
- void addAtIndex(int index, int val) 将一个值为 val 的节点插入到链表中下标为 index 的节点之前。如果 index 等于链表的长度,那么该节点会被追加到链表的末尾。如果 index 比长度更大,该节点将 不会插入 到链表中。
- void deleteAtIndex(int index) 如果下标有效,则删除链表中下标为 index 的节点。
题意:(这道题涵盖了链表的基本操作)
在链表类中实现这些功能:
- get(index):获取链表中第 index 个节点的值。如果索引无效,则返回-1。
- addAtHead(val):在链表的第一个元素之前添加一个值为 val 的节点。插入后,新节点将成为链表的第一个节点。
- addAtTail(val):将值为 val 的节点追加到链表的最后一个元素。
- addAtIndex(index,val):在链表中的第 index 个节点之前添加值为 val 的节点。如果 index 等于链表的长度,则该节点将附加到链表的末尾。如果 index 大于链表长度,则不会插入节点。如果index小于0,则在头部插入节点。
- deleteAtIndex(index):如果索引 index 有效,则删除链表中的第 index 个节点。
示例:
解题思路分析
建议统一采取虚拟头节点的方式,这样节点的操作会一致。
涉及到的链表操作:
- 获取第n个节点
- 头部插入节点
- 尾部插入节点
- 第n个节点之前插入节点
- 删除第n个节点
注意题目中的下标是从0开始的;
获取第n个节点:
n从0开始,首先需要对n进行一个合法判断,n<0和 n>size-1,都是不合法的
需要定义一个临时指针来遍历链表(不能直接操作头节点,因为遍历完了还要返回头节点,直接操作头节点的话头指针的值都改了,无法返回头节点了)
代码讲解:
//创建虚拟头节点
ListNode dummy = new ListNode(-1,head);
//定义一个临时指针来遍历链表
ListNode cur = dummy.next;
//size是int类型,用来存储链表元素个数
// 寻找第n个节点的值,这里要注意空指针异常等情况,如果拿不准while里的条件,可以简单举个例子带入一下,就知道对不对了,这里n>0而不是n>=0,是因为等于0的时候不需要进入循环,直接返回头节点值即可
while(n > 0) {
cur = cur.next;
n--;
}
return cur.val;
头部插入节点:
代码讲解:
//创建新节点,并赋值
ListNode toAdd = new ListNode(val);
//这里易错点是指针赋值的顺序问题,要先让新节点指向它的下一个节点;
//如果先让dummy的next指向新节点,那么后面的链表就会丢失,因为此时已经没有指向下一个节点的指针
toAdd.next = dummy.next;
//让虚拟头节点指向新节点
dummy.next = toAdd;
//这里头节点由于有一个head指针,因此不会丢失,但是如果是其他位置插入节点,顺序错了就会丢失
head = toAdd;
//链表元素个数+1
size++;
尾部插入节点:
代码讲解:
//创建新节点,并赋值,此时默认next指针为null
ListNode toAdd = new ListNode(val);
cur = dummy;
//遍历找到尾部节点,当cur.next为空,就找到了尾部节点
while(cur.next != null) {
cur = cur.next;
}
cur.next = toAdd;
//链表元素个数+1
size++;
第n个节点前插入节点:
要在第n个节点前插入节点,那么就一定得知道第n个节点的前一个节点位置;
然后让新节点指向第n个节点,让前一个节点指向新节点即可;
代码讲解:
//创建新节点,并赋值,此时默认next指针为null
ListNode toAdd = new ListNode(val);
cur = dummy;
//寻找第n个节点,让cur指向第n个节点的前一个节点
while(n > 0) {
cur = cur.next;
n--;
}
//先让新节点指向第n个节点
toADD = cur.next;
//再让第n个节点的前一个节点指向新节点
cur.next = toAdd;
//链表元素个数+1
size++;
删除第n个节点:
此处别忘记判断n是否合法;要找到第n个节点的前一个节点;
代码讲解:
//要找到n的前一个节点的指针
cur = dummy;
while(n > 0) {
cur = cur.next;
n--;
}
//删除第n个:让原先的n-1节点直接指向n+1节点
cur.next = cur.next.next;
//链表元素个数-1
size--;
总结:
易混淆点:
cur指针的初始位置:
当获取第n个节点的时候,临时指针cur是从dummy.next开始遍历的,因此拿到的是第n个节点的值;
当获取第n个节点前一个节点的时候,临时指针cur是从dummy开始遍历的,因此拿到的是第n-1个节点的值;
易错点:
插入和删除操作的时候,一定要先更新靠后位置的节点指针,再更新靠前位置的节点指针,否则会导致链表元素丢失;
题解:
设计单链表:
设计双链表:
双链表的易错点仍然在赋值顺序上:
插入操作:
- 先搞定s节点的前驱和后继;
- 再搞定后节点的前驱;
- 前节点的后继。
如果第四步放到第二步和第三步之前执行,就会导致p.next提前变成了s,插入失败
删除操作:
- 搞定前节点的后继;
- 搞定后节点的前驱。
题解: