基础知识
1. 概念:
链表是一种通过指针串联在一起的线性结构,每一个节点由两部分组成,一个是数据域一个是指针域(存放指向下一个节点的指针)。链表的入口节点称为链表的头结点(head),最后一个节点的指针域指向null(空指针)。
public class ListNode {
int val;
ListNode next;
// 节点的构造函数(无参)
public ListNode() {
}
// 节点的构造函数(有一个参数)
public ListNode(int val) {
this.val = val;
}
// 节点的构造函数(有两个参数)
public ListNode(int val, ListNode next) {
this.val = val;
this.next = next;
}
}
2. 类别:
-
单列表
-
双链表:每一个节点有两个指针域,一个指向下一个节点,一个指向上一个节点。既可以向前查询也可以向后查询。
-
循环链表 :链表首尾相连,可以用来解决约瑟夫环问题
3. 存储方式
链表通过指针域的指针链接在内存中的各个节点,所以链表中的节点在内存中不是连续分布的 ,而是散乱分布在内存中的某地址上,分配机制取决于操作系统的内存管理。
4. 链表基本操作
(1). 增加
(2). 删除
5. 与数组对比
——————链表的所有操作都要注意防止断链———————
一、移除链表元素
删除节点,头结点需特殊考虑,链表的其他节点都是通过前一个节点来移除当前节点,而头结点没有前一个节点。两种解决方法:
1. 将头结点向后移动一位:
while (head != null && head.val == val) {
head = head.next;
}
public ListNode removeElements(ListNode head, int val) {
while (head != null && head.val == val) {
head = head.next;
}
// 已经为null,提前退出
if (head == null) {
return head;
}
// 已确定当前head.val != val
ListNode pre = head;
ListNode cur = head.next;
while (cur != null) {
if (cur.val == val) {
pre.next = cur.next;
} else {
pre = cur;
}
cur = cur.next;
}
return head;
}
2. 设置虚拟头结点,统一操作
ListNode newHead = new ListNode(-1, head); // 虚拟头结点
ListNode pre = newHead; // 虚拟头结点
ListNode cur = head; // 真实头结点
class Solution {
public ListNode removeElements(ListNode head, int val) {
if(head == null){
return null;
}
ListNode newHead = new ListNode(-1, head);
ListNode pre = newHead;
ListNode cur = head;
while(cur != null){
if(cur.val == val){
pre.next = cur.next;
}else{
pre = cur;
}
cur = cur.next;
}
return newHead.next;
}
}
二、设计链表
1. 单链表
class ListNode {
int val;
ListNode next;
ListNode(){}
ListNode(int val) {
this.val=val;
}
}
class MyLinkedList {
int size; //size存储链表元素的个数
ListNode head; //虚拟头结点
public MyLinkedList() { //初始化链表
size = 0;
head = new ListNode(0);
}
public int get(int index) {
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);
}
public void addAtIndex(int index, int val) {
if (index > size) { // 越界
return;
}
if (index < 0) { // 插在头结点前
index = 0;
}
//找到要插入节点的前驱
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;
size++;
}
public void deleteAtIndex(int index) {
if (index < 0 || index >= size) { // 索引非法
return;
}
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;
size--;
}
}
2. 双链表:插入注意先将自己搭在链表上,再改动原有的链接
class ListNode{
int val;
ListNode next,prev;
ListNode() {};
ListNode(int val){
this.val = val;
}
}
class MyLinkedList {
int size;
ListNode head,tail; //记录链表的虚拟头结点和尾结点
public MyLinkedList() {
//初始化操作
this.size = 0;
this.head = new ListNode(0);
this.tail = new ListNode(0);
//这一步非常关键,否则在加入头结点的操作中会出现null.next的错误!!!
head.next=tail;
tail.prev=head;
}
public int get(int index) {
//判断index是否有效
if(index<0 || index>=size){
return -1;
}
ListNode cur = this.head;
//判断是哪一边遍历时间更短
if(index >= size / 2){
//tail开始,从后向前
cur = tail;
for(int i=0; i< size-index; i++){
cur = cur.prev;
}
}else{ //cur开始,从前向后
for(int i=0; i<= index; i++){
cur = cur.next;
}
}
return cur.val;
}
public void addAtHead(int val) {
//等价于在第0个元素前添加
addAtIndex(0,val);
}
public void addAtTail(int val) {
//等价于在最后一个元素(null)前添加
addAtIndex(size,val);
}
public void addAtIndex(int index, int val) {
//index大于链表长度
if(index>size){
return;
}
//index小于0
if(index<0){
index = 0;
}
size++;
//找到前驱
ListNode pre = this.head;
for(int i=0; i<index; i++){
pre = pre.next;
}
//新建结点
ListNode newNode = new ListNode(val);
newNode.next = pre.next;
pre.next.prev = newNode;
newNode.prev = pre;
pre.next = newNode;
}
public void deleteAtIndex(int index) {
//判断索引是否有效
if(index<0 || index>=size){
return;
}
//删除操作
size--;
ListNode pre = this.head;
for(int i=0; i<index; i++){
pre = pre.next;
}
pre.next.next.prev = pre;
pre.next = pre.next.next;
}
}
三、翻转链表
1.双指针法:cur指针遍历链表,pre指针指向cur的前一个节点,改变指向前用tmp暂存cur.next,防止断链。
class Solution {
public ListNode reverseList(ListNode head) {
ListNode cur = head;
ListNode pre = null;
ListNode tmp = null;
while(cur != null){
tmp = cur.next; // tmp暂存cur.next,防止断链
cur.next = pre; // 改变指向
pre = cur; // 后移,注意这两句的顺序不能换
cur = tmp;
}
return pre;
}
}
2. 递归法:与双指针类似
class Solution {
public ListNode reverseList(ListNode head) {
return reverse(null, head);
}
private ListNode reverse(ListNode prev, ListNode cur) {
if (cur == null) {
return prev;
}
ListNode temp = null;
temp = cur.next;// 保存下一个节点
cur.next = prev;// 反转
// 更新prev、cur位置
// prev = cur;
// cur = temp;
return reverse(cur, temp);
}
}
四、两两反转
使用虚拟头结点,画图
class Solution {
public ListNode swapPairs(ListNode head) {
if(head == null || head.next == null){ // 为空或只有一个节点
return head;
}
ListNode dumyhead = new ListNode(-1); // 虚拟头结点
dumyhead.next = head;
ListNode cur = dumyhead;
ListNode temp; // 临时节点,保存两个节点后面的节点防止断链(第三个节点)
ListNode firstnode; // 临时节点,保存两个节点之中的第一个节点
ListNode secondnode; // 临时节点,保存两个节点之中的第二个节点
while(cur.next != null && cur.next.next != null){
firstnode = cur.next;
secondnode = cur.next.next;
temp = cur.next.next.next;
cur.next = secondnode; // 第一步
secondnode.next = firstnode; // 第二步
firstnode.next = temp; // 第三步
}
return dumyhead.next;
}
}
五、删除倒数第n个节点-双指针
让fast移动n步,让fast和slow相差n个节点,然后让fast和slow同时移动,直到fast指向链表末尾。删掉slow所指向的节点。
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode dummyNode = new ListNode(0); // 虚拟头结点
dummyNode.next = head;
ListNode fast = dummyNode;
ListNode slow = dummyNode;
for(int i = 0; i <= n; i++){ // 中间相差n个节点,需移动n+1步
fast = fast.next;
}
while(fast != null){ // 同时后移
slow = slow.next;
fast = fast.next;
}
//此时 slow 的位置就是待删除元素的前一个位置
slow.next = slow.next.next;
return dummyNode.next;
}
}
六、链表相交
思路:两个链表A、B,令指针curA指向链表A的头结点,curB指向链表B的头结点,求出两个链表的长度,并求出两个链表长度的差值,然后让curA移动到,和curB 末尾对齐的位置,比较curA和curB是否相同,如果不相同,同时向后移动curA和curB,如果遇到curA == curB,则找到交点。否则循环退出返回空指针。
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
int lenA = 0, lenB = 0;
ListNode curA = headA;
ListNode curB = headB;
// 求链表长
while(curA != null){
lenA++;
curA = curA.next;
}
while(curB != null){
lenB++;
curB = curB.next;
}
curA = headA; // 记得重新指回头结点
curB = headB;
// 令A长
if(lenB > lenA){
//1. swap (lenA, lenB);
int tmpLen = lenA;
lenA = lenB;
lenB = tmpLen;
//2. swap (curA, curB);
ListNode tmpNode = curA;
curA = curB;
curB = tmpNode;
}
int gap = lenA - lenB; // 长度差
for(int i = 1; i <= gap; i++){
curA = curA.next;
}
while(curA != null){
if(curA == curB){
return curA;
}
curA = curA.next;
curB = curB.next;
}
return null;
}
}
七、环形链表II-双指针
思路:从头结点出发,fast指针每次移动两个节点,slow指针每次移动一个节点,如果 fast 和 slow指针在途中相遇 ,说明这个链表有环。
为什么一定在环中相遇?1. fast指针一定先进入环中,如果fast指针和slow指针相遇的话,一定是在环中相遇;2. 因为fast是走两步,slow是走一步,其实相对于slow来说,fast是一个节点一个节点的靠近slow的,所以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;
}
}
八、回文链表
反转后半段链表,和前半段链表相比
class Solution {
public boolean isPalindrome(ListNode head) {
// 反转后半段链表,和前半段链表相比
if(head==null || head.next==null){
return true;
}
// 找中点-slow
ListNode fast = head;
ListNode slow = head;
while (fast.next != null && fast.next.next != null) {
fast = fast.next.next;
slow = slow.next; // 前半段最后一个结点
}
ListNode post = Reverse(slow.next); // 后半段第一个节点
ListNode p1 = head;
ListNode p2 = post;
boolean result = true;
while(result && p2 != null) {
if (p1.val != p2.val) {
result = false;
}
p1 = p1.next;
p2 = p2.next;
}
slow.next = Reverse(post); // 还原链表
return result;
}
public ListNode Reverse(ListNode head) {
ListNode tmp = null;
ListNode cur = head;
ListNode pre = null;
while(cur != null) {
tmp = cur.next;
cur.next = pre;
pre = cur;
cur = tmp;
}
return pre;
}
}
九、删除链表重复元素
有两种问题:剩一个重复元素和只要重复就全删除
十、二叉树转单链表
class Solution {
public void flatten(TreeNode root) {
// 将左子树插入到右子树的地方
// 将原来的右子树接到左子树的最右边节点
while(root != null) {
if(root.left == null) { //左子树为 null,直接考虑下一个节点
root = root.right;
}else {
TreeNode pre = root.left;
while(pre.right != null) {
pre = pre.right; // 找左子树最右边的节点
}
pre.right = root.right; //将原来的右子树接到左子树的最右边节点
root.right = root.left; // 将左子树插入到右子树的地方
root.left = null;
root = root.right; // 考虑下一个节点
}
}
}
}