06-链表相关面试题

链表相关面试题

链表问题

面试时链表解题的方法论

1)对于笔试,不用太在乎空间复杂度,一切为了时间复杂度

2)对于面试,时间复杂度依然放在第一位,但是一定要找到空间最省的方法

链表面试题常用数据结构和技巧

1)使用容器(哈希表、数组等)

2)快慢指针

快慢指针

1)输入链表头节点,奇数长度返回中点,偶数长度返回上中点

2)输入链表头节点,奇数长度返回中点,偶数长度返回下中点

3)输入链表头节点,奇数长度返回中点前一个,偶数长度返回上中点前一个

4)输入链表头节点,奇数长度返回中点前一个,偶数长度返回下中点前一个

注意边界条件

  • 输入链表头节点,奇数长度返回中点,偶数长度返回上中点
// head 头
public static Node midOrUpMidNode(Node head) {
    if (head == null || head.next == null || head.next.next == null) {
        return head;
    }
    // 链表有3个点或以上
    Node slow = head.next;
    Node fast = head.next.next;
    while (fast.next != null && fast.next.next != null) {
        slow = slow.next;
        fast = fast.next.next;
    }
    return slow;
}
  • 输入链表头节点,奇数长度返回中点,偶数长度返回下中点
public static Node midOrDownMidNode(Node head) {
    if (head == null || head.next == null) {
        return head;
    }
    Node slow = head.next;
    Node fast = head.next;
    while (fast.next != null && fast.next.next != null) {
        slow = slow.next;
        fast = fast.next.next;
    }
    return slow;
}
  • 输入链表头节点,奇数长度返回中点前一个,偶数长度返回上中点前一个
public static Node midOrUpMidPreNode(Node head) {
    if (head == null || head.next == null || head.next.next == null) {
        return null;
    }
    Node slow = head;
    Node fast = head.next.next;
    while (fast.next != null && fast.next.next != null) {
        slow = slow.next;
        fast = fast.next.next;
    }
    return slow;
}
  • 输入链表头节点,奇数长度返回中点前一个,偶数长度返回下中点前一个
public static Node midOrDownMidPreNode(Node head) {
    if (head == null || head.next == null) {
        return null;
    }
    if (head.next.next == null) {
        return head;
    }
    Node slow = head;
    Node fast = head.next;
    while (fast.next != null && fast.next.next != null) {
        slow = slow.next;
        fast = fast.next.next;
    }
    return slow;
}

常见链表面试题

题目1: 判断链表是否为回文结构

上面说了,链表常用技巧主要就是使用容器(哈希表、数组等)和快慢指针

所以有3个方法。

方法1:建栈比对

将所有节点压入栈中,然后与链表节点一一对比。

相当于逆序了,然后逆序与正序比较,相同为回文结构

public static boolean isPalindrome1(Node head) {
		Stack<Node> stack = new Stack<Node>();
		Node cur = head;
		while (cur != null) {
			stack.push(cur);
			cur = cur.next;
		}
		while (head != null) {
			if (head.value != stack.pop().value) {
				return false;
			}
			head = head.next;
		}
		return true;
	}

方法2:快慢指针加建栈

使用快慢指针定位到链表中间,然后将链表右半部分建栈,一一比较。

当然偶数长度的话,就截下中点

public static boolean isPalindrome2(Node head) {
		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;
		}
		Stack<Node> stack = new Stack<Node>();
		while (right != null) {
			stack.push(right);
			right = right.next;
		}
		while (!stack.isEmpty()) {
			if (head.value != stack.pop().value) {
				return false;
			}
			head = head.next;
		}
		return true;
	}

方法3:纯指针操作,空间复杂度O (1)

快慢指针找到链表中点。

从中点开始,将中点后面的指针指向逆序。并再将中点指向null作为终止条件。

例如:1–> 2 – >3 --> 2–> 1就变为1–> 2–> 3 <–2 <-- 1

然后将中点3,指向null,偶数长度2选一,随便中点一个指向null

指针逆序完成后。

一个head从头,一个tail从尾开始一一比较,head或tail任意一个为null时停止。

之前比较时,记录一下尾节点,因为现在恢复节点,我们就重新从尾节点开始恢复。

	public static boolean isPalindrome3(Node head) {
		//边界条件
		if (head == null || head.next == null) {
			return true;
		}
		Node n1 = head;
		Node n2 = head;
		while (n2.next != null && n2.next.next != null) { // find mid node
			n1 = n1.next; // n1 -> mid
			n2 = n2.next.next; // n2 -> end
		}
		// n1 中点
		
		
		n2 = n1.next; // n2 -> right part first node
		n1.next = null; // mid.next -> null
		Node n3 = null;
		while (n2 != null) { // right part convert
			n3 = n2.next; // n3 -> save next node
			n2.next = n1; // next of right node convert
			n1 = n2; // n1 move
			n2 = n3; // n2 move
		}

		// 记录n1尾节点,对比完成后,从n1开始更改会原来的指向
		n3 = n1;
		n2 = head;// n2 -> left first node
		boolean res = true;
		while (n1 != null && n2 != null) { // check palindrome
			if (n1.value != n2.value) {
				res = false;
				break;
			}
			n1 = n1.next; // left to mid
			n2 = n2.next; // right to mid
		}
		n1 = n3.next;
		n3.next = null;
		while (n1 != null) { // recover list
			n2 = n1.next;
			n1.next = n3;
			n3 = n1;
			n1 = n2;
		}
		return res;
	}

题目2:链表做荷兰国旗问题

有2种方法

方法1:倒入数组做partition,然后链起来

partition,快排的核心函数

此方法只为AC,没考虑空间复杂度。

代码大体为:

先得到链表长度,然后new出相同长度的数组,并循环往数组里拷贝值。

然后将这个数组做partition。

然后将数组元素取出,链起来

public static Node listPartition1(Node head, int pivot) {
		if (head == null) {
			return head;
		}
		Node cur = head;
		int i = 0;
		while (cur != null) {
			i++;
			cur = cur.next;
		}
		Node[] nodeArr = new Node[i];
		i = 0;
		cur = head;
		for (i = 0; i != nodeArr.length; i++) {
			nodeArr[i] = cur;
			cur = cur.next;
		}
		arrPartition(nodeArr, pivot);
		for (i = 1; i != nodeArr.length; i++) {
			nodeArr[i - 1].next = nodeArr[i];
		}
		nodeArr[i - 1].next = null;
		return nodeArr[0];
	}

	public static void arrPartition(Node[] nodeArr, int pivot) {
		int small = -1;
		int big = nodeArr.length;
		int index = 0;
		while (index != big) {
			if (nodeArr[index].value < pivot) {
				swap(nodeArr, ++small, index++);
			} else if (nodeArr[index].value == pivot) {
				index++;
			} else {
				swap(nodeArr, --big, index);
			}
		}
	}

	public static void swap(Node[] nodeArr, int a, int b) {
		Node tmp = nodeArr[a];
		nodeArr[a] = nodeArr[b];
		nodeArr[b] = tmp;
	}

方法2:链表操作

方法2只使用了有限的几个变量

先声明6个node,分别为小于区的head(sH),tail(sT),等于区的head(eH),tail(eT),大于区的head(mH),tail(mT)。

每个区都有两个节点,一个记录头,一个记录尾。然后对链表进行遍历比较,当前节点值小于就放入小于区。

如果小于区为空,就同时赋给sH,sT。如果小于区又来了个node,将将其链到尾节点sT下,sT.next = 新来的node。然后sT = 新来的node,保持sT一直在链表尾。

当链表遍历结束,所有节点都到了属于自己的区。于是场上便有3个区的3个链表。

我们只需要将3个链表链起来,便可以了。只不过链的时候要注意考虑判断是否存在空链表的情况。

public static Node listPartition2(Node head, int pivot) {
		Node sH = null; // small head
		Node sT = null; // small tail
		Node eH = null; // equal head
		Node eT = null; // equal tail
		Node mH = null; // big head
		Node mT = null; // big tail
		Node next = null; // save next node
		
		while (head != null) {
			next = head.next;
			head.next = null;
			if (head.value < pivot) {
				if (sH == null) {
					sH = head;
					sT = head;
				} else {
					sT.next = head;
					sT = head;
				}
			} else if (head.value == pivot) {
				if (eH == null) {
					eH = head;
					eT = head;
				} else {
					eT.next = head;
					eT = head;
				}
			} else {
				if (mH == null) {
					mH = head;
					mT = head;
				} else {
					mT.next = head;
					mT = head;
				}
			}
			head = next;
		}
		// 小于区域的尾巴,连等于区域的头,等于区域的尾巴连大于区域的头
		if (sT != null) { // 如果有小于区域
			sT.next = eH;
			eT = eT == null ? sT : eT; // 下一步,谁去连大于区域的头,谁就变成eT
		}
		// 下一步,一定是需要用eT 去接 大于区域的头
		// 有等于区域,eT -> 等于区域的尾结点
		// 无等于区域,eT -> 小于区域的尾结点
		// eT 尽量不为空的尾巴节点
		if (eT != null) { // 如果小于区域和等于区域,不是都没有
			eT.next = mH;
		}
		return sH != null ? sH : (eH != null ? eH : mH);
	}

题目3: 深度复制带有rand指针的链表

题目如下:

LeetCode题目连接:https://leetcode-cn.com/problems/copy-list-with-random-pointer/

—种特殊的单链表节点类描述如下:

public class Node {
		int val;
		Node next;
		Node random;

		public Node(int val) {
			this.val = val;
			this.next = null;
			this.random = null;
		}
	}

random指针是单链表节点结构中新增的指针,random可能指向链表中的任意一个节点,也可能指向null。
给定一个由Node节点类型组成的无环单链表的头节点 head,请实现一个函数完成这个链表的复制,并返回复制的新链表的头节点。

【要求】时间复杂度O(N).额外空间复杂度O(1)

此问题依然有两个方法,依靠HashMap的数据结构和纯指针操作

方法1:HashMap

将链表节点复制,并放到HashMap中。key为原链表的老节点,value为复制的新节点。

然后我们可以去遍历链表,

通过map,去找到key的next,赋给对应value的next,key的rand赋给value的rand。

循环结束,复制完成。

public static Node copyRandomList1(Node head) {
		// key 老节点
		// value 新节点
		HashMap<Node, Node> map = new HashMap<Node, Node>();
		Node cur = head;
		while (cur != null) {
			map.put(cur, new Node(cur.val));
			cur = cur.next;
		}
		cur = head;
		while (cur != null) {
			// cur 老
			// map.get(cur) 新
			// 新.next ->  cur.next克隆节点找到
			map.get(cur).next = map.get(cur.next);
			map.get(cur).random = map.get(cur.random);
			cur = cur.next;
		}
		return map.get(head);
	}

方法2:链表构建对应关系

方法1,是通过hashmap构建对应关系,进行复制。而我们可以在链表上直接进行操作,构建对应关系。

思路为:

将每个节点复制,连接到对应节点后

如1–>2–>3–>4–>5,复制并链接就成为 1–>1’–>2–>2’–>3–>3’–>4–>4’–>5–>5’ ( 的为复制的节点)

这样也构建了对应关系,我们可以一对一对的取出进行操作。

例如1的rand节点指向3,那么1’就应该指向3’,因为其相邻,即:1’.rand==1.rand.next。

循环构建完对应的rand节点后。

我们便可以对链表进行分离操作

public static Node copyRandomList2(Node head) {
		if (head == null) {
			return null;
		}
		Node cur = head;
		Node next = null;
		// 1 -> 2 -> 3 -> null
		// 1 -> 1' -> 2 -> 2' -> 3 -> 3'
		while (cur != null) {
			next = cur.next;
			cur.next = new Node(cur.val);
			cur.next.next = next;
			cur = next;
		}
		cur = head;
		Node copy = null;
		// 1 1' 2 2' 3 3'
		// 依次设置 1' 2' 3' random指针
		while (cur != null) {
			next = cur.next.next;
			copy = cur.next;
			copy.random = cur.random != null ? cur.random.next : null;
			cur = next;
		}
		Node res = head.next;
		cur = head;
		// 老 新 混在一起,next方向上,random正确
		// next方向上,把新老链表分离
		while (cur != null) {
			next = cur.next.next;
			copy = cur.next;
			cur.next = next;
			copy.next = next != null ? next.next : null;
			cur = next;
		}
		return res;
	}

题目4: 两个可能有环的单链表相交的第一个节点

此问题有几种可能:

1.两个无环链表是否相交,找相交节点。不相交,返回null

2.两个有环链表,找相交节点。不相交,返回null

3.如果一个是有环链表,一个是无环链表。不用判断,这两个链表必不相交。

准备操作,判断链表是否有环,返回入环节点

在对上面3中可能进行操作前,首先我们需要有一个能判断链表是否有环的方法。

并返回入环节点。

通过快慢指针实现,slow和fast指针,slow一次走一步,fast一次走两步。

当slow与fast相遇时。

fast回到链表头,slow依然呆在相遇点。

然后fast,slow再次一起移动,不过这次两者都是一次走一步。

然后两者再次相遇时,便是入环点。

	// 找到链表第一个入环节点,如果无环,返回null
	public static Node getLoopNode(Node head) {
		if (head == null || head.next == null || head.next.next == null) {
			return null;
		}
		// n1 慢  n2 快
		Node slow = head.next; // n1 -> slow
		Node fast = head.next.next; // n2 -> fast
		while (slow != fast) {
			if (fast.next == null || fast.next.next == null) {
				return null;
			}
			fast = fast.next.next;
			slow = slow.next;
		}
		// slow fast  相遇
		fast = head; // n2 -> walk again from head
		while (slow != fast) {
			slow = slow.next;
			fast = fast.next;
		}
		return slow;
	}

然后先看第一种,两个无环链表

大致思路为:

先声明一个int数,用来记录2个链表长度的差值。

将2个链表分别遍历,一个n++,一个n–,最后n的值,就是两个链表长度的差值。

2个链表都遍历到尾部,我们先比较尾部节点,如果不相等,则不相交,直接返回。

如果相等,则相交,继续操作找出相交点。

选择较长的一个链表,先走n(长度差)步。然后2个链表同时++,进行移动。

当2个节点相等,即是相交点。

// 如果两个链表都无环,返回第一个相交节点,如果不想交,返回null
	public static Node noLoop(Node head1, Node head2) {
		if (head1 == null || head2 == null) {
			return null;
		}
		Node cur1 = head1;
		Node cur2 = head2;
		int n = 0;
		while (cur1.next != null) {
			n++;
			cur1 = cur1.next;
		}
		while (cur2.next != null) {
			n--;
			cur2 = cur2.next;
		}
		if (cur1 != cur2) {
			return null;
		}
		// n  :  链表1长度减去链表2长度的值
		cur1 = n > 0 ? head1 : head2; // 谁长,谁的头变成cur1
		cur2 = cur1 == head1 ? head2 : head1; // 谁短,谁的头变成cur2
		n = Math.abs(n);
		while (n != 0) {
			n--;
			cur1 = cur1.next;
		}
		while (cur1 != cur2) {
			cur1 = cur1.next;
			cur2 = cur2.next;
		}
		return cur1;
	}

两个有环链表

如果是两个有环链表,找向交点,存在3种情况。

1.两个有环链表不相交

2.两个有环链表相交,且入环点相同

3.两个有环链表相交,但入环点不相同。

首先解决情况2,入环点相同。

如果入环点相同,我们可以将入环点设为end截止,然后用上面noLoop的方式去找相交点。

情况1和情况3,可以一起解决。

随便用一个入环点,循环一次,去找另一个入环点,找到了,直接返回。没找到则是没相交

//参数loop为入环点
public static Node bothLoop(Node head1, Node loop1, Node head2, Node loop2) {
		Node cur1 = null;
		Node cur2 = null;
		if (loop1 == loop2) {
			cur1 = head1;
			cur2 = head2;
			int n = 0;
			while (cur1 != loop1) {
				n++;
				cur1 = cur1.next;
			}
			while (cur2 != loop2) {
				n--;
				cur2 = cur2.next;
			}
			cur1 = n > 0 ? head1 : head2;
			cur2 = cur1 == head1 ? head2 : head1;
			n = Math.abs(n);
			while (n != 0) {
				n--;
				cur1 = cur1.next;
			}
			while (cur1 != cur2) {
				cur1 = cur1.next;
				cur2 = cur2.next;
			}
			return cur1;
		} else {
			cur1 = loop1.next;
			while (cur1 != loop1) {
				if (cur1 == loop2) {
					return loop1;
				}
				cur1 = cur1.next;
			}
			return null;
		}
	}

调用方法

上面写了3个方法,我们需要一个方法进行整合

先获取两个链表的入环点

如果都没环,进行noLoop判断

如果有环,进行bothLoop判断。

如果一个有环,一个没环,直接return null,不相交。

public static Node getIntersectNode(Node head1, Node head2) {
		if (head1 == null || head2 == null) {
			return null;
		}
		Node loop1 = getLoopNode(head1);
		Node loop2 = getLoopNode(head2);
		if (loop1 == null && loop2 == null) {
			return noLoop(head1, head2);
		}
		if (loop1 != null && loop2 != null) {
			return bothLoop(head1, loop1, head2, loop2);
		}
		return null;
	}

题目5: 不给单链表的头节点删除指定节点

此问题如标题所示:

一个单链表,不给头节点。只给要删除的节点,让你删除。

有个取巧的方法,可以把删除节点的下一个节点的值,覆盖要删除的值,然后删除下一个节点。

但这种方法并不是真正的删除。

实际上,不给单链表的头节点删除指定节点的操作,是不可能的。

要删除就要给头节点。


06-链表相关面试题
https://blog.isle.ink//archives/lian-biao-xiang-guan-mian-shi-ti
作者
Administrator
发布于
2022年07月28日
许可协议