面试题18:删除链表的节点
① 题目一:在 O ( 1 ) O(1) O(1)时间内删除链表节点
- 原本的思想:
- 从头节点开始,遍历链表查找到待删除节点的前驱节点,更改前驱节点的next指针即可。
- 需要判断待删除节点的位置:是头节点、非头节点,同时还有O(n)的时间复杂度。
- 要求使用
O
(
1
)
O(1)
O(1)的时间复杂度,这时有个巧妙的方法:将该节点的后继节点的值赋值给自身,更改自身的next指针以删除重复值的后继节点。
- 特殊情况: 如果待删除节点的后继节点是
null
,无法进行赋值,直接将待删除节点置为null
即可。 - 代码:
// 只告诉了待删除的节点,只能使用O(1)的删除算法
public void deleteNode(ListNode node) {
// 待删除的节点是尾节点,直接将其置为null
if (node.next == null) {
node = null;
} else {// 待删除的节点不是尾节点,复制后继节点的值并删除后继节点
node.val = node.next.val;
node.next = node.next.next;
}
}
② 题目二:删除链表中重复的节点
递归实现:
- 递归停止的条件为
pHead == null || pHead.next == null
,直接返回pHead
。 - 如果从头节点开始就存在重复节点,则不停移动头指针直到元素不重复或者后继节点为
null
。然后对head.next
链表进行递归删除,删除后的结果作为当前链表的头节点并返回。 - 否则,
head.next
指向对head.next
进行递归删除后的结果,并返回head
。
- 代码如下:
public ListNode deleteDuplication(ListNode pHead) {
if (pHead == null || pHead.next == null) {
return pHead;
}
// 头节点开始就存在重复节点
if (pHead.val == pHead.next.val) {
while (pHead.next != null && pHead.val == pHead.next.val) {
pHead = pHead.next;
}
// 指向下一个新的节点,递归删除重复节点
return deleteDuplication(pHead.next);
} else {// 不存在重复节点
pHead.next = deleteDuplication(pHead.next);
return pHead;
}
}
③ 相似题 —— leetcode203:移除链表元素
- 如果原链表中的节点满足要求,则直接访问下一个节点;否则,借助新的头节点,将不满足要求的节点都插入到新链表中。
- 代码如下:
public ListNode removeElements(ListNode head, int val) {
ListNode newHead = new ListNode(0);
ListNode p = newHead;
// 如果head为null不执行循环,无需进行特殊情况判断
while (head != null) {
if (head.val != val) {// 值不相等,将节点移动到新链表中
p.next = head;
p = p.next;
}
head = head.next;// 更新head指针,指向下一个待判断的节点
}
p.next = null;
return newHead.next;
}
面试题19:正则表达式匹配
- 使用动态规划:
- 简历二位动态数组,
dp[i][j]
表示s[i]
与p[i]
是否匹配。 - 初始化:s和p都为空时,
dp[0][0] = true
;如果s为空且p不为空,如果p[i]
为*
,则dp[0][i+1]=dp[0][i-1]
,即后退两个字符。 - 计算时,如果当前字符匹配或者p[j]为
.
,则dp[i+1][j+1]
取决于前一次匹配的结果。 - 如果p[j]为
*
,则比较p中的前一个字符,不满足条件则视a*
为空;其他情况,视a*
为a(dp[i+1][j]
)、aa(dp[i][j+1]
)、空(dp[i+1][j-1]
)。
public boolean isMatch(String s, String p) {
int m = s.length(), n = p.length();
boolean[][] dp = new boolean[m + 1][n + 1];
dp[0][0] = true;// s和p长度均为0的情况
// s长度为0,p 的长度不为0
for (int i = 0; i < n; i++) {
if (p.charAt(i) == '*') {
dp[0][i + 1] = dp[0][i - 1];// 后退2个字符
}
}
// s和p均不为0的情况
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (s.charAt(i) == p.charAt(j) || p.charAt(j) == '.') {
dp[i + 1][j + 1] = dp[i][j];// 同时匹配,结果取决于前一次结果
}else if (p.charAt(j) == '*') {// p中的字符为*,比较前一个字符
if (s.charAt(i) != p.charAt(j-1) && p.charAt(j-1) != '.') {
dp[i + 1][j + 1] = dp[i + 1][j - 1];// 跳过p中的字符
} else {
dp[i + 1][j + 1] = (dp[i + 1][j]// a*作为单独的a
|| dp[i + 1][j - 1]// a*作为空
|| dp[i][j + 1]);// a*作为aa
}
}
}
}
return dp[m][n];
}
- 使用Java API,只有一句话,但是时间消耗了120ms,而动态规划只消耗了9ms。
public boolean isMatch(String s, String p) {
return s.matches(p);
}
面试题20:表示数值的字符串
- 根据示例,可以知道:
- 首尾的空格不影响字符串的判断,而中间的空格则影响字符串的判断。
- 应该先去除首尾的空格,然后判断字符串长度是否为0以及中间是否包含空格。
- 根据执行结果可以知道,可能存在
2.334f
的情况,这时应该为false,因此接着判断字符串是否包含f
。 - 然后将字符串按照
e
分割为两部分,第一部分可能为整数或小数,第二部分必须为整数,整数应该为long
而非int
。 - 具体的数值转换使用包装类的valueOf()方法,如果出现
NumberFormatException
异常则说明格式不正确,返回false
。
- 代码:
public boolean isNumber(String s) {
// 先去除首尾的空格,如果新的字符串长度为0,
// 或者中间含有空格,或者含有f,则格式错误
if (s.length() == 0) {
return false;
}
s = s.trim();
if (s.contains(" ")) {
return false;
} else if (s.contains("f")) {
return false;
}
// 按照e进行分割,分别判断字符串是否符合要求
if (s.contains("e")) {
String fisrt = s.substring(0, s.indexOf('e'));
String second = s.substring(s.indexOf('e') + 1, s.length());
return (isInteger(fisrt) || isDouble(fisrt)) && isInteger(second);
} else if (s.contains(".")) {
return isDouble(s);
} else {
return isInteger(s);
}
}
public boolean isInteger(String s) {
try {
Long.valueOf(s);
} catch (NumberFormatException e) {
return false;
}
return true;
}
public boolean isDouble(String s) {
try {
Double.valueOf(s);
} catch (NumberFormatException e) {
return false;
}
return true;
}
面试题21:调整数组顺序是奇数位于偶数前面
① 不保证元素相对顺序
- 最简单的想法: 使用双指针,一个从前往后查找偶数,一个从后往前查找奇数;都找到后,交换奇数和偶数。缺点: 无法保证奇数和奇数、偶数和偶数之间相对位置不变。
- 代码:
public void reOrderArray(int[] array) {
int i = 0, j = array.length - 1;
while (i < j) {
while (i < j && (array[i] & 1) != 0) {// 通过与1判断元素的奇偶性
i++;
}
while (i < j && (array[j] & 1) == 0) {
j--;
}
if (i < j) {
int temp = array[i];
array[i] = array[j];
array[j] = temp;
}
}
}
② 保证元素相对顺序
- 最笨的方法,新开辟一个数组:
- 统计数组中的奇数的个数
- 将之前的数组复制给新数组,然后将新数组中的元素按照奇偶性分别从
0
和oldLen
开始放置。
- 代码如下,时间复杂度 O ( n ) O(n) O(n),空间复杂度 O ( n ) O(n) O(n)。
public void reOrderArray(int[] array) {
int oldCount = 0;
for (int i = 0; i < array.length; i++) {
if ((array[i] & 1) != 0) {
oldCount++;
}
}
int[] copyArr = Arrays.copyOf(array, array.length);
int start = 0;
for (int i = 0; i < copyArr.length; i++) {
if ((copyArr[i] & 1) != 0) {
array[start++] = copyArr[i];
} else {
array[oldCount++] = copyArr[i];
}
}
}
③ 相似题 —— leetcode328:链表中元素的调整
- 将第一个节点视为奇节点,第二个节点视为偶节点,以此类推。将奇节点放在偶节点的前面,并保持原有的顺序不变。
- 穿针法: old指针每次将奇节点穿起来,even指针每次将偶节点穿起来,最后将old指针指向偶链表的头部。停止穿针的条件:
even
或者even.next
为null
。
- 代码如下:
public ListNode oddEvenList(ListNode head) {
if (head == null || head.next == null) {//可以只考虑head为null的情况
return head;
}
ListNode old = head, even = head.next;
ListNode evenStart = even;// 保存偶链的头节点,用于奇链和偶链的对接
while (even != null && even.next != null) {
// 先穿奇节点,再穿偶节点
old.next = old.next.next;
even.next = even.next.next;
old = old.next;
even = even.next;
}
// 两链对接
old.next = evenStart;
return head;
}
面试题22:链表中倒数第k个节点
- 笨办法:两次遍历。
- 第一次遍历:统计链表中的节点个数
n
。 - 第二次遍历:倒数第
k
个节点,即正数的n - k + 1
个节点,找到n - k + 1
个节点即可。
- 快慢指针:
- 快指针先走k步,然后快慢指针一起移栋,直到快指针指向
null
节点。 - 如果快指针在走k步的过程中,达到了null节点,说明倒数第
k
个节点不存在,直接返回null
。
public ListNode FindKthToTail(ListNode head, int k) {
ListNode fast = head;
while (fast != null && k > 0) {// 快指针先走k步
fast = fast.next;
k--;
}
if (k > 0) {// 倒数第k个节点超过链表长度
return null;
}
ListNode slow = head;
while (fast != null) {// 快慢指针一起走
slow = slow.next;
fast = fast.next;
}
return slow;
}
题目变形:leetcode876:求链表的中间节点
- 快慢指针: 快指针每次走两步,慢指针每次走一步,当
fast
或fast.next
为null
时,慢指针走到了中间节点。 - 题目的特殊之处:
- 链表非空,不用进行特殊情况的判断。
- 如果中间节点有两个,必须返回第二个。这样的话,如果
fast.next
为null
时,fast
只能走一步。
- 代码如下:
public ListNode middleNode(ListNode head) {
ListNode fast = head.next, slow = head;
// 要求同样的中间节点返回第二个
while (fast != null) {
if (fast.next != null) {
fast = fast.next.next;
} else {
fast = fast.next;
}
slow = slow.next;
}
return slow;
}