最近chatgpt热度很高,恰巧团队在招新同学,正好尝试下把面试同学的其中一个问题让chatgpt回答一边,看看回答质量如何。 岗位为测试开发岗,要求基本扎实,要有较强的测试思维。 我在平时的面试中,不会问候选人一些偏门的或者刁钻的问题(例如刷leetcode题库),本着问此类问题除了装X别无它用的原则,我会挑选一些有代表性的问题与候选人进行共同探 讨拓以及深入优化,发掘候选人的基础知识掌握以及潜力。
面试评分标准: 总分 10 分
这里我们不用太多的考虑候选人背过题目源码实现的情况,经验表明,大部分背过题目的候选人对于代码的运行逻辑部分回答的并不理想,另外需要通过其他 题目辅助判断候选人的掌握情况
- 编码 5 分 :代码优雅,能够写出代码,并且能够详细的解释代码运行逻辑给 5分
- 单测 5 分 :单测考虑充分,正常和异常逻辑考虑充分并且能够给出设计的合理解释 5分
guoheng:用java实现一个链表的反转
ChatGPT3.5:这里提供一种基于迭代的反转链表的Java实现方法:
class ListNode {
int val;
ListNode next;
ListNode(int x) { val = x; }
}
public class Solution {
public ListNode reverseList(ListNode head) {
ListNode prev = null;
ListNode curr = head;
while (curr != null) {
ListNode nextTemp = curr.next;
curr.next = prev;
prev = curr;
curr = nextTemp;
}
return prev;
}
}
解释:
我们定义一个指向当前节点的指针curr
、一个指向上一个节点的指针prev
和一个指向下一个节点的指针nextTemp
。在每次迭代中,我们将curr.next
指向prev
,然后将prev
和curr
指针向右移动一位。重复这个过程,直到curr
指向null
为止,返回prev
。
guoheng:针对以上实现,编写单测用例,考虑尽可能多的场景
ChatGPT3.5:好的,以下是一些可能的单测用例:
- 测试空链表的反转,期望结果是
null
ListNode head = null;
Solution solution = new Solution();
ListNode reversedList = solution.reverseList(head);
assertNull(reversedList);
- 测试只有一个节点的链表的反转
ListNode head = new ListNode(1);
Solution solution = new Solution();
ListNode reversedList = solution.reverseList(head);
assertEquals(1, reversedList.val);
assertNull(reversedList.next);
- 测试有多个节点的链表的反转
ListNode head = new ListNode(1);
head.next = new ListNode(2);
head.next.next = new ListNode(3);
Solution solution = new Solution();
ListNode reversedList = solution.reverseList(head);
assertEquals(3, reversedList.val);
assertEquals(2, reversedList.next.val);
assertEquals(1, reversedList.next.next.val);
assertNull(reversedList.next.next.next);
- 测试当链表只有两个节点时的反转
ListNode head = new ListNode(1);
head.next = new ListNode(2);
Solution solution = new Solution();
ListNode reversedList = solution.reverseList(head);
assertEquals(2, reversedList.val);
assertEquals(1, reversedList.next.val);
assertNull(reversedList.next.next);
- 测试反转链表后原链表是否被修改
ListNode head = new ListNode(1);
head.next = new ListNode(2);
head.next.next = new ListNode(3);
Solution solution = new Solution();
ListNode reversedList = solution.reverseList(head);
assertNotSame(head, reversedList);
assertNull(head.next);
- 测试链表中存在重复元素的情况
ListNode head = new ListNode(1);
head.next = new ListNode(2);
head.next.next = new ListNode(2);
head.next.next.next = new ListNode(3);
Solution solution = new Solution();
ListNode reversedList = solution.reverseList(head);
assertEquals(3, reversedList.val);
assertEquals(2, reversedList.next.val);
assertEquals(2, reversedList.next.next.val);
assertEquals(1, reversedList.next.next.next.val);
assertNull(reversedList.next.next.next.next);
希望这些单测用例能帮助您验证代码的正确性。
guoheng:测试反转链表后原链表是否被修改再细化下
ChatGPT3.5:请看以下示例代码:
ListNode head = new ListNode(1);
head.next = new ListNode(2);
head.next.next = new ListNode(3);
Solution solution = new Solution();
ListNode reversedList = solution.reverseList(head);
assertEquals(3, reversedList.val);
assertEquals(2, reversedList.next.val);
assertEquals(1, reversedList.next.next.val);
assertNull(reversedList.next.next.next);
assertNull(head.next.next); // 原链表被修改
在这个单测用例中,我们首先创建一个由三个节点组成的链表
head
,然后将其传递给Solution.reverseList()
方法以反转链表。接着我们检查反 转后的链表是否符合期望,并确保原链表是否被修改。由于Java中对象是通过引用传递的,因此如果反转链表算法直接更改原链表中的节点,那么原链表的next
指针将指向null
,表明链表已结束。因此,我们可以在单测中检查原链表的最后一个节点的next
指针是否为null
来判断原链表是否 被修改。在这个例子中,我们检查head.next.next
是否为null
。如果是,那么原链表已经被修改。
guoheng:以上解法不对,head.next已经是null了,head.next.next会运行报错
ChatGPT3.5:非常抱歉,您是正确的。我犯了一个错误,在
head.next
上进行了两次转换检查。代码应该只检查head.next
是否为null
, 而不是head.next.next
。在确保原链表未被修改的单测用例中,代码应该是这样的:
ListNode head = new ListNode(1);
head.next = new ListNode(2);
head.next.next = new ListNode(3);
Solution solution = new Solution();
ListNode reversedList = solution.reverseList(head);
assertEquals(3, reversedList.val);
assertEquals(2, reversedList.next.val);
assertEquals(1, reversedList.next.next.val);
assertNull(reversedList.next.next);
assertNull(head.next); // 原链表未被修改
感谢您的指正,希望这个修正能够帮助更好地解决问题。
guoheng:以上只判断了head是否被交换,没有判断剩下的节点是否被交换,继续优化下
ChatGPT3.5:可以使用递归来判断剩下的节点是否被交换。具体做法是,从递归函数返回的节点开始往前逐个比较,如果有任何一个节点与原始链表不 同,就说明链表中发生了交换,返回交换后的头节点;否则返回原始头节点。以下是优化后的代码:
ListNode* swapPairs(ListNode* head) {
if (head == nullptr || head->next == nullptr) {
return head;
}
ListNode* newHead = head->next;
head->next = swapPairs(newHead->next);
newHead->next = head;
return newHead;
}
guoheng:总结一下测试反转链表后原链表是否被修改的方法
ChatGPT3.5:总的来说,测试反转链表后原链表是否被修改,可以采用以下方法:
- 备份原始链表头节点
- 反转链表
- 比较反转后的链表与原始链表是否相同
- 恢复原始链表
具体实现方法有多种,可以使用递归或迭代的方式来反转链表。在比较链表是否相同时,可以使用递归或迭代的方式来逐个比较节点的值和指针。 在使用递归反转链表时,需要注意递归终止条件的判断,如果链表为空或只有一个节点,直接返回头节点。同时,在递归返回时,需要判断链表是否发生了交换, 可以通过判断头节点是否被交换来判断。如果头节点被交换了,说明链表中发生了交换,需要返回交换后的头节点;否则,返回原始头节点。 在使用迭代反转链表时,需要使用三个指针分别指向当前节点、前一个节点和后一个节点,然后不断地将当前节点的 next 指针指向前一个节点,并将三个指针 向后移动,直到当前节点为空。在迭代结束后,头节点变成了原链表的尾节点,需要保存新的头节点并返回。
guoheng:在判断原始链表是否被反转时,只比较与反转后的链表是否相同是不够的,请继续优化
ChatGPT3.5:是的,只比较反转后的链表与原始链表是否相同是不够的,因为在反转链表的过程中,可能会修改原始链表的节点值。因此,需要在比较 链表是否相同时,同时比较节点值和指针是否相同。
具体实现方法有多种,以下是一种可行的方法:
- 备份原始链表头节点
- 反转链表
- 遍历反转后的链表和备份的原始链表,同时比较节点的值和指针是否相同
- 恢复原始链表
以下是优化后的代码示例:
bool isLinkedListReversed(ListNode* head) {
// 备份原始头节点
ListNode* originalHead = head;
// 反转链表
ListNode* prev = nullptr;
ListNode* curr = head;
while (curr != nullptr) {
ListNode* next = curr->next;
curr->next = prev;
prev = curr;
curr = next;
}
ListNode* newHead = prev;
// 比较链表是否相同
while (originalHead != nullptr && newHead != nullptr) {
if (originalHead->val != newHead->val || originalHead->next != newHead->next) {
return false;
}
originalHead = originalHead->next;
newHead = newHead->next;
}
// 恢复原始链表
prev = nullptr;
curr = newHead;
while (curr != nullptr) {
ListNode* next = curr->next;
curr->next = prev;
prev = curr;
curr = next;
}
head = prev;
return true;
}
这段代码中,先备份原始链表的头节点,然后采用迭代方式反转链表,然后遍历反转后的链表和原始链表,同时比较节点的值和指针是否相同。如果有任何一 个节点不同,就返回false,否则返回true。最后,再次使用迭代方式恢复原始链表。
guoheng:只比较节点指针是否相同也是不够的,原始链表的节点指针和交换后的链表节点指针是头尾相等的,请继续优化
ChatGPT3.5:是的,如果只比较节点指针是否相同也是不够的,因为原始链表的节点指针和交换后的链表节点指针是头尾相等的,这样就会导致在比 较时判断相等,从而给出错误的结果。因此,需要在比较节点指针时,同时比较节点在链表中的位置。
具体实现方法有多种,以下是一种可行的方法:
- 备份原始链表头节点
- 反转链表
- 遍历反转后的链表和备份的原始链表,同时比较节点的值和节点在链表中的位置是否相同
- 恢复原始链表
以下是优化后的代码示例:
bool isLinkedListReversed(ListNode* head) {
// 备份原始头节点
ListNode* originalHead = head;
int len = 0;
while (originalHead != nullptr) {
originalHead = originalHead->next;
len++;
}
originalHead = head;
// 反转链表
ListNode* prev = nullptr;
ListNode* curr = head;
while (curr != nullptr) {
ListNode* next = curr->next;
curr->next = prev;
prev = curr;
curr = next;
}
ListNode* newHead = prev;
// 比较链表是否相同
int i = 0;
while (originalHead != nullptr && newHead != nullptr) {
if (originalHead->val != newHead->val || len - i != i + 1) {
return false;
}
originalHead = originalHead->next;
newHead = newHead->next;
i++;
}
// 恢复原始链表
prev = nullptr;
curr = newHead;
while (curr != nullptr) {
ListNode* next = curr->next;
curr->next = prev;
prev = curr;
curr = next;
}
head = prev;
return true;
}
这段代码中,先备份原始链表的头节点,并计算链表的长度。然后采用迭代方式反转链表,然后遍历反转后的链表和原始链表,同时比较节点的值和节点在链表中的位置是否相同。如果有任何一个节点不同,就返回false,否则返回true。最后,再次使用迭代方式恢复原始链表。
guoheng:原始链表的节点指针和交换后的链表节点指针存在对应关系,利用该对应关系做节点指针的判读,继续优化
ChatGPT3.5:是的,如果能够利用原始链表的节点指针和交换后的链表节点指针存在对应关系来判断,那么就可以更加高效地解决问题。具体实现方法如下:
- 备份原始链表头节点
- 反转链表
- 遍历反转后的链表,同时比较原始链表的当前节点与交换后的链表的当前节点、原始链表的下一个节点与交换后的链表的上一个节点是否相同
- 恢复原始链表
以下是优化后的代码示例:
bool isLinkedListReversed(ListNode* head) {
// 备份原始头节点
ListNode* originalHead = head;
// 反转链表
ListNode* prev = nullptr;
ListNode* curr = head;
while (curr != nullptr) {
ListNode* next = curr->next;
curr->next = prev;
prev = curr;
curr = next;
}
ListNode* newHead = prev;
// 比较链表是否相同
while (originalHead != nullptr && newHead != nullptr) {
if (originalHead == newHead || originalHead->next == newHead) {
break;
}
if (originalHead->next != nullptr && originalHead->next == newHead->next) {
break;
}
originalHead = originalHead->next;
newHead = newHead->next;
}
// 恢复原始链表
prev = nullptr;
curr = newHead;
while (curr != nullptr) {
ListNode* next = curr->next;
curr->next = prev;
prev = curr;
curr = next;
}
head = prev;
return originalHead == newHead;
}
这段代码中,先备份原始链表的头节点。然后采用迭代方式反转链表,然后遍历反转后的链表和原始链表,同时比较当前节点和下一个节点的指针是否匹配。具体来说,如果原始链表的当前节点与交换后的链表的当前节点相同,或者原始链表的下一个节点与交换后的链表的上一个节点相同,那么就跳出循环。否则就继续循环,直到遍历完链表。最后,再次使用迭代方式恢复原始链表。最后,如果原始链表的当前节点和交换后的链表的当前节点相同,就返回true,否则返回false。
这种方法的时间复杂度为O(n),空间复杂度为O(1)。
guoheng:使用java来实现上述代码
ChatGPT3.5:好的,以下是使用Java实现的代码示例:
public boolean isLinkedListReversed(ListNode head) {
// 备份原始头节点
ListNode originalHead = head;
// 反转链表
ListNode prev = null;
ListNode curr = head;
while (curr != null) {
ListNode next = curr.next;
curr.next = prev;
prev = curr;
curr = next;
}
ListNode newHead = prev;
// 比较链表是否相同
while (originalHead != null && newHead != null) {
if (originalHead == newHead || originalHead.next == newHead) {
break;
}
if (originalHead.next != null && originalHead.next == newHead.next) {
break;
}
originalHead = originalHead.next;
newHead = newHead.next;
}
// 恢复原始链表
prev = null;
curr = newHead;
while (curr != null) {
ListNode next = curr.next;
curr.next = prev;
prev = curr;
curr = next;
}
head = prev;
return originalHead == newHead;
}
这段代码与之前的C++实现方法非常相似,只是语法上有一些细微的差别。
- 编码部分:代码优雅、运行逻辑解释的合理,满足 5 分标准
- 单测部分: 测试用例部分,能够考虑大部分异常,经过几次的提示后能够给出单测的优化以及合理解释,但是距离满分还有差距, 例如:最后一个用例的本意是链表的节点经过交换后, 1->2->3->null 3->2->1->null 原始链表节点第一个节点的引用和反转后链表的 第3个节点是同一个引用的关系,这里考察的是,链表反转是要进行的是节点的交换,而不是节点值的交换,测试用例中并没有很好的理解节点交换和节点值 交换的区别,所以可以给到3.5~4分的评价
如果单从以上题目的回答,chatgpt的是超越了大部分候选人的,因为此问题的解答在互联网上非常多所以chatgpt能够达到如此的程度我并不十分的意外, 同时,单测用例的这部分是我在面试候选人的过程中总结加入的,互联网上也没有太多此类的的总结,故猜测chatgpt没有太多此类的训练,尤其是节点交换和节点值交换部分。
综上,经过对chatgpt的使用以及我尝试了一些工作中的具体场景后,我暂时还不太担心chatgpt能够取代我的工作,反而chatgpt能够成为一个很好的生产 力提升工具,尤其是对于一些经验型的知识总结。