[算法入门笔记] 3. 链表

1. 方法论

链表解题方法论:

  • 对于笔试:一切为了时间复杂度,不用在乎空间复杂度
  • 对于面试:时间复杂度放在首位,空间复杂度尽量最小

重要技巧

  • 额外数据结构记录(哈希表等)
  • 快慢指针

2. 链表回文

方式1 利用快慢指针走到中点,将链表后一半放入栈

public boolean isPalindrome(Node head) {
	// 链表为空或者只有单个结点时,返回true
	if (head == null || head.next == null) {
		return true;
	}
	// 定义右指针
	Node right = head.next;
	// 定义当前指针
	Node cur = head;
	while (cur.next != null && cur.next.next != null) {
		// 慢指针每次走一步
		right = right.next;
		// 快指针每次走两步
		cur = cur.next.next;
	}
	// 将链表的后半部分装入栈中
	while (right != null) {
		stack.push(right);
		right = right.next;
	}
	while (!stack.isEmpty()) {
		if (head.value != satck.pop().value) {
			return false;
		}
		head = head.next;
	}
	return true;
}

方式2 利用快慢指针走到中点,将链表后一部分反转,双指针遍历

public static boolean isPalidrome(Node head) {
	// 链表为空或者只有单个结点时,返回true
	if (head == null || head.next == null) {
        return true;
    }
    // 定义慢指针n1
    Node n1 = head;
    // 定义快指针n2
    Node n2 = head;
    while (n2.next != null && n2.next.next != null) {
     	// 慢指针每次走一步
        n1 = n1.next;
        // 快指针每次走两步
        n2 = n2.next.next;
        // 当快指针走到链表末尾,慢指针便到达链表中点
    }
    // n2此时指向右半部分链表的第一个结点
    n2 = n1.next;
    // 令中点的后继为空,断链
    n1.next = null;
    // 定义n3 辅助转置
    Node n3 = null;
    // 右半部分链表转置
     while (n2 != null) {
        // n3用于保存后继结点
        n3 = n2.next;
        // 转置操作
        n2.next = n1;
        // n1指向n2
        n1 = n2;
        // n2指向n3
        n2 = n3;
    }
    // n3指向转置后的链表的(从左至右)最后一个结点
    n3 = n1;
    // n2指向转置后的链表的(从左至右)第一个结点
    n2 = head;
    boolean res = true;
    // 检查回文
    while (n1 != null && n2 != null) {
        if (n1.value != n2.value) {
            res = false;
            break;
        }
        // 从左到中点
        n1 = n1.next;
        // 从右到中点
        n2 = n2.next;
    }
    n1 = n3.next;
    n3.next = null;
    // 复原链表
    while (n1 != null) {
        n2 = n1.next;
        n1.next = n3;
        n3 = n1;
        n1 = n2;
    }
    return res;
}

3. 链表划分

将单向链表按某值划分成左边小、中间相等、右边大的形式
[题目] 给定一个单链表的头节点head,节点的值类型是整型,再给定一个整数pivot。实现一个调整链表的函数,将链表调整为左部分都是值小于pivot的节点,中间部分都是值等于pivot的节点,右部分都是值大于pivot的节点

[进阶] 在实现原问题功能的基础上增加如下的要求

  • 调整后所有小于pivot的节点之间的相对顺序和调整前一样
  • 调整后所有等于pivot的节点之间的相对顺序和调整前一样
  • 调整后所有大于pivot的节点之间的相对顺序和调整前一样
  • 时间复杂度请达到O(N),额外空间复杂度请达到o(1)。
    [参考图解]
    在这里插入图片描述
public Node listPartition(Node head, int pivot) {
	// 定义sH、sT(小于pivot的部分)
	Node sH = null; Node sT = null;
	// 定义eH、eT(等于pivot的部分)
    Node eH = null; Node eT = null;
    // 定义bH、bT(大于pivot的部分)
    Node bH = null; Node bT = null;
    Node next = null;

	while (head != null) {
		// 断链
		next = head.next;
		head.next = null;
		// 如果当前值小于枢轴
		if (head.value < pivot) {
			// 如果左半段部分没有任何结点时
			// s头指针sH和s尾指针sT都指向head
			if (sH == null) {
				sH = head;
				sT = head;
			} else {
			    // s尾指针sT连接当前节点
				sT.next = head;
				// 更新s尾指针sT
				sT = head;
			}
		} else if (head.value == pivot) { // 如果当前值等于枢轴
			// 如果中间段部分没有任何结点时
			// e头指针eH和e尾指针eT都指向head
			if (eH == null) {
                eH = head;
                eT = head;
            } else {
            	// e尾指针eT连接当前节点
                eT.next = head;
                // 更新e尾指针eT
                eT = head;
            }
		} else { // 如果当前值大于枢轴
     		// 如果右半部分没有任何结点时
			// b头指针bH和b尾指针bT都指向head
            if (bH == null) {
                bH = head;
                bT = head;
            } else {
                // b尾指针bT连接当前节点
                bT.next = head;
                // 更新b尾指针eT
                bT = head;
            }
        }
        // 更新当前节点位置
        head = next;
	}
	// 如果存在左半段链表[详解图解]
	if (sT != null) {
		// s尾指针sT后继指向e头指针eH
        sT.next = eH;
        eT = eT == null ? sT : eT;
    } 
    // 如果左半段链表和中间段链表不是都没有[详解图解]
    if (eT != null){
        eT.next = bH;
    }
    return sH != null ? sH : eH != null ? eH : bH;
}

[此处图解,后续补充,近期较忙]

4. 复制含有随机指针结点的链表

[题目] 定义一种特殊单链表结点类,描述如下:

class Node {
	int value;
	Node next;
	Node rand;
	Node(int val) {
		value = val;
	}
}

rand指针是单链表结点结构中新增的指针,rand可能指向链表中的任意一个结点,也可能指向null。给定一个由Node结点类型组成的无环单链表的头结点head,请实现一个函数完成这个链表的复制,并返回复制的新链表的头结点。
[要求] 时间复杂度O(N),额外空间复杂度O(1)

方式1 哈希表

[参考图解]
在这里插入图片描述

public Node copyListWithRandom(Node head) {
	// 构建原始结点映射至克隆新结点的哈希表
	HashMap<Node, Node> map = new HashMap<>();
	Node cur = head;
	// 拷贝原始链表
	while (cur != null) {
		map.put(cur, new Node(cur.value));
		cur = cur.next;
	}
	cur = head;
	while (cur != null) {
		// 拷贝结点的next域指向原始结点next域对应的拷贝结点
		map.get(cur).next = map.get(cur.next);
		// 拷贝结点的rand域指向原始结点rand域对应的拷贝结点
		map.get(cur).rand = map.get(cur.rand);
		cur = cur.next;
	}
	return map.get(head);
}

5.链表相交问题

[题目] 给定两个可能有环也可能无环的单链表,头结点head1和head2。请实现一个函数,如果两个链表相交,请返回相交的第一个结点。如果不相交,返回null。
[要求] 如果两个链表长度和为N,时间复杂度达到O(N),额外空间复杂度达到O(1)

5.1 单个链表有环问题

[Q1] 如何判断一个链表有环,如果有,返回第一个入环口,没有返回null

算法思想

  • 设置快慢指针,快指针走两步,慢指针走一步
  • 如果没有环快指针会先到达终点
  • 如果有环,快指针与慢指针会在环中某一位置相
  • 当快慢指针相遇时,快指针指向head,与慢指针同步移动,会在第一个入环口相遇。
public Node getLoopNode(Node head) {
	// 头结点为空或者只有单个头结点时返回null
	if (head == null || head.next == null) {
		return null;
	}
	// 定义慢指针
	Node slow = head.next;
	// 定义快指针
	Node fast = head.next.next;
	while (fast != slow) {
		if (fast.next == null || fast.next.next == null) {
			return null;
		}
		fast = fast.next.next;
		slow = slow.next;
	}
	// 此时fast指向head
	fast = head;
	// 此时slow与fast同步移动
	while (fast != slow) {
		fast = fast.next;
		slow = slow.next;
	}
	return slow;
}

解决了问题1就知道两个链表有环或者无环情况。如果一个链表有环,另一个链表无环,那么这个链表是无论也不可能相交的。能相交的就是两个情况:

  • 两个链表都无环(Q2)
  • 两个链表都有环(Q3)

5.2 两个无环链表相交问题

[Q2] 如何判断两个无环链表是否相交,相交则返回第一个相交节点,不相交返回null

算法思想

  • a. 指针1从头结点开始,走到最后一个结点(不是结束),统计链表1的长度len1,记录链表1的最后一个结点为end1
  • b. 链表2从头结点开始,走到最后一个结点(不是结束),统计链表2的长度len2,记录链表2的最后给一个结点end2
  • c. 如果end1!=end2,说明两个链表不相交,返回null即可;如果end1==end2,说明两个链表相交,进入步骤d. 来寻找第一个相交的结点
  • d. 如果链表1比较长,链表1就先走len1-len2步;如果链表2比较长,链表2就先走len2-len1步。然后两个链表一起走,两个链表第一次走到一起的结点就是第一个相交的结点
public Node noLoop(Node head1, Node head2) {
	if (head1 == null || head2 == null) {
		return null;
	}
	Node cur1 = null;
	Node cur2 = null;
	int len = 0;
	// 求链表1的长度
	while (cur1.next != null) {
		len++;
		cur1 = cur1.next;
	}
	// 求链表1与链表2的长度差
	while (cur2.next != null) {
		len--;
		cur2 = cur2.next;
	}
	if (cur1 != cur2) {
		return null;
	}
	// cur1选择长的链表
	cur1 = n > 0 ? head1 : head2;
	// cur2选择短的链表
	cur2 = cur1== head1 ? head2 : head1;
	len = Math.abs(len);
	while (len != 0) {
		len--;
		cur1 = cur1.next;
	}
	// 两指针同步移动
	while (cur1 != cur2) {
		cur1 = cur1.next;
		cur2 = cur2.next;
	}
	return cur1;
}

5.3 两个有环链表相交问题

[Q3] 如何判断两个有环链表相交,相交则返回第一个相交节点,不相交返回null
[loop1 == loop2 两个链表相交图示]
在这里插入图片描述
[loop1 != loop2 两个链表不相交图示]
在这里插入图片描述
[loop1 != loop2两个链表相交图示]
在这里插入图片描述

算法思想

  • 让链表1从入环扣1(loop1)出发(由于loop1和之后的所有结点都在环熵,将来一定能返回loop1)
  • 如果回到loop1之前没有遇到loop2,说明两个链表结构如图[loop1 != loop2 两个链表不相交图示],即不相交,返回null
  • 如果回到loop1之前遇到loop2,说明两个链表结构如图[loop1 != loop2两个链表相交图示],也就是相交
  • 由于loop1和loop2都在两天链表上,只不过loop1是离链表1较近的结点,loop2是离链表2较近的结点,返回两者中任意一个即可
public Node bothLoop(Node head1, Node head2, Node loop1, Node loop2){
	Node cur1 = null;
	Node cur2 = null;
	// 图示1
	if (loop1 == loop2) {
		cur1 = head1;
		cur2 = head2;
		int len = 0;
		while (cur1 != loop1) {
			len++;
			cur = cur1.next;
		}
		while (cur2 != loop2) {
			len--;
			cur2 = cur1.next; 
		}
		cur1 = len > 0 ? head1 : head2;
		cur2 = cur1 == head1 ? head2 : head1;
		len = Math.abs(len);
		while (len != 0) {
			len--;
			cur1 = cur.next;
		}
		while (cur1 != cur2) {
			cur1 = cur1.next;
			cur2 = cur2.next;
		}
		return cur1;
	} else { // 图示2 3
		cur1 = loop1.next;
		while (cur1 != loop1) {
			if (cur1 == loop2) {
				return loop1;
			}
			cur1 = cur1.next;
		}
		return null;
	}
}

[综上]

public class Node {
	public int value;
	public Node next;
	public Node(int value) {
		this.value = value;
	}
}
public Node getIntersectNode(Node head1, Node head2) {
	// 两个链表头结点为空 直接返回null
    if (head1 == null || head2 == null) {
        return null;
    }
    // 判断第一个链表有没有环
    Node loop1 = getLoopNode(head1);
    // 判断第一个链表有没有环
    Node loop2 = getLoopNode(head2);
    // 情况1
    if (loop1 == null && loop2 == null) {
        return noLoop(head1, head2);
    }
    // 情况2/3
    if (loop1 != null && loop2 != null) {
        return bothLoop(head1, loop1, head2, loop2);
    }
    return null;
}

待更新~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Cyan Chau

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值