链表
1. 理论基础
链表就是指针串联在一起的线性结构,每个节点有数据域和指针域。链接的入口节点称为链表的头结点也就是head。
链表的类型:
- 单链表
- 双链表:每个节点都有两个指针域,一个指向下一个节点,一个指向上一个节点。向前、后都可以查询。
- 循环链表:链表首尾相连。(可以解决约瑟夫环问题
链表在内存中不是连续分布的。
定义链表节点方式(做题会给出,但是自己要会写
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; }
}
在做链表题时,最好都设置一个虚拟头节点来进行统一操作。
2. 移除链表元素
题目:给一个链表的头结点head和整数val,删除链表中等于给定值 val 的所有节点,并返回新的头结点。
题目分析:删除可能会涉及到头结点,所以可以设置一个虚拟节点来统一操作
class Solution {
public ListNode removeElements(ListNode head, int val) {
if(null == head){
return head;
}
//设置一个虚拟节点来进行统一操作
ListNode dummy = new ListNode(-1, head);
ListNode pre = dummy;
ListNode cur = head;
while(cur != null){
if(val == cur.val){
pre.next = cur.next;
}else{
pre = cur;
}
cur = cur.next;
}
return dummy.next; //这才是新的头结点
}
}
3. 设计链表
题目:链表实现一些基本功能。
在链表类中实现这些功能:
- get(index):获取链表中第 index 个节点的值。如果索引无效,则返回-1。
- addAtHead(val):在链表的第一个元素之前添加一个值为 val 的节点。插入后,新节点将成为链表的第一个节点。
- addAtTail(val):将值为 val 的节点追加到链表的最后一个元素。
- addAtIndex(index,val):在链表中的第 index 个节点之前添加值为 val 的节点。如果 index 等于链表的长度,则该节点将附加到链表的末尾。如果 index 大于链表长度,则不会插入节点。如果index小于0,则在头部插入节点。
- deleteAtIndex(index):如果索引 index 有效,则删除链表中的第 index 个节点。
题目分析:自己选择设计单链表还是双链表。以下设置一个虚拟节点来统一操作。
class MyLinkedList {
//单链表
class ListNode{
int val;
ListNode next;
ListNode(){}
ListNode(int val){
this.val = val;
}
}
//size存储链表元素的个数
int size;
//虚拟头结点
ListNode head;
//初始化链表
MyLinkedList(){
size = 0;
head = new ListNode(0);
}
//获取第index个节点的数值
public int get(int index) {
//如果index非法,返回-1
if(index < 0 || index >= size){
return -1;
}
ListNode cur = head;
//包含一个虚拟头结点,所以查找第index + 1 个节点
for(int i = 0; i <= index; i++){
cur = cur.next;
}
return cur.val;
}
//在链表最前面插入一个节点
public void addAtHead(int val) {
addAtIndex(0, val);
}
//在链表最后插入一个节点
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 pre = head;
for(int i = 0; i < index; i++){
pre = pre.next;
}
ListNode toAdd = new ListNode(val);
toAdd.next = pre.next;
pre.next = toAdd;
}
//删除第index个节点
public void deleteAtIndex(int index) {
if(index < 0 || index >= size){
return ;
}
size--;
ListNode pre = head;
for(int i = 0; i < index; i++){
pre = pre.next;
}
pre.next = pre.next.next;
}
}
/**
* 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);
*/
4. 反转链表
题目:反转一个单链表。
题目分析:cur指向头节点,pre初始化为null,cur->next指向pre,在这之前要把它保存下来。cur指向null,也就反转完成了,return pre即可。
//双指针方法(掌握住
class Solution {
public ListNode reverseList(ListNode head) {
ListNode cur = head;
//两个节点一定要初始化
ListNode pre = null;
ListNode temp = null;
while(cur != null){
temp = cur.next;
cur.next = pre;
pre = cur;
cur = temp;
}
return pre;
}
}
//递归方法(有点抽象 了解一下
class Solution {
public ListNode reverseList(ListNode head) {
return reverse(null, head);
}
private ListNode reverse(ListNode pre, ListNode cur) {
if (cur == null) {
return pre;
}
ListNode temp = null;
temp = cur.next;// 先保存下一个节点
cur.next = pre;// 反转
// 更新prev、cur位置
// prev = cur;
// cur = temp;
return reverse(cur, temp);
}
}
// 从后向前递归
//没看懂 不太能理解(留着万一以后看懂了呢
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;
}
}
5. 两两交换链表中的节点
题目:给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。
题目分析:画图理清楚操作的前后顺序。
//递归方法(好简便但是不太能理解
class Solution {
public ListNode swapPairs(ListNode head) {
//退出提交
if(null == head || null == head.next){
return head;
}
//获取当前节点的下一个节点
ListNode after = head.next;
//进行递归
ListNode newNode = swapPairs(after.next);
//进行交换
after.next = head;
head.next = newNode;
return after;
}
}
// 虚拟头结点
class Solution {
public ListNode swapPairs(ListNode head) {
ListNode dummyNode = new ListNode(0);
dummyNode.next = head;
ListNode prev = dummyNode;
while (prev.next != null && prev.next.next != null) {
ListNode temp = head.next.next; // 缓存 next
//下面三个就对应了图上的三个步骤
prev.next = head.next; // 将 prev 的 next 改为 head 的 next
head.next.next = head; // 将 head.next(prev.next) 的next,指向 head
head.next = temp; // 将head 的 next 接上缓存的temp
prev = head; // 步进1位
head = head.next; // 步进1位
}
return dummyNode.next;
}
}
6. 删除链表的倒数第N个节点
题目:给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。
题目分析:双指针的经典应用:设置一个dummy,fast和slow都指向它。fast先走n + 1步,直到fast.next == null,则slow此时为要删除节点的前驱。
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode dummy = new ListNode(0, head);
ListNode fast = dummy;
ListNode slow = dummy;
for(int i = 0; i < n; i++){
fast = fast.next;
}
while(fast.next != null){
fast = fast.next;
slow = slow.next;
}
slow.next = slow.next.next;
return dummy.next;
}
}
7. 链表相交
题目:给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null 。注意:函数返回结果后,链表必须 保持其原始结构 。
题目分析:求两个链表交点节点的指针,是指针相等而不是数值相等。步骤:求出两个链表的长度,缩短差值让其两个链表指针对齐,这样再开始比较是否相同。
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
ListNode curA = headA;
ListNode curB = headB;
int lenA = 0, lenB = 0;
//求链表A的长度
while(curA != null){
lenA++;
curA = curA.next;
}
//求链表B的长度
while(curB != null){
lenB++;
curB = curB.next;
}
curA = headA;
curB = headB;
//指定curA是最长链表的头结点,LenA是最长链表的长度
if(lenB > lenA){
//交换长度
int temp = lenA;
lenA = lenB;
lenB = temp;
//交换节点
ListNode tempNode = curA;
curA = curB;
curB = tempNode;
}
//长度差
int gap = lenA - lenB;
//让两个指针对齐
while(gap-- > 0){
curA = curA.next;
}
//遍历,有相同节点就返回
while(curA != null){
if(curA == curB){
return curA;
}
curA = curA.next;
curB = curB.next;
}
return null;
}
}
8. 环形链表||
题目:给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。为了表示给定链表中的环,使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。
题目分析:
- 判断是否有环:快慢指针,fast走两步,slow走一步,最后相遇就表明有环。
- 有环怎么找到环的入口:结论:从头结点出发一个指针,从相遇节点 也出发一个指针,这两个指针每次只走一个节点, 那么当这两个指针相遇的时候就是 环形入口的节点。
public class Solution {
public ListNode detectCycle(ListNode head) {
ListNode slow = head;
ListNode fast = head;
while(fast != null && fast.next != null){
slow = slow.next;
fast = fast.next.next;
//有环
if(slow == fast){
ListNode index1 = fast;
ListNode index2 = head;
//两个指针从头结点和相遇节点各走一步,相遇点就是环的入口
while(index1 != index2){
index1 = index1.next;
index2 = index2.next;
}
return index1;
}
}
return null;
}
}