剑指Offer面试题:12.在O(1)时间删除链表结点
一、题目:在O(1)时间删除链表结点
题目:给定单向链表的头指针和一个结点指针,定义一个函数在O(1)时间删除该结点。
原文采用的是C/C++,这里采用C#,节点定义如下:
public class Node<T> { // 数据域 public T Item { get; set; } // 指针域 public Node<T> Next { get; set; } public Node() { } public Node(T item) { this.Item = item; } }
要实现的DeleteNode方法定义如下:
public static void DeleteNode(Node<int> headNode, Node<int> deleteNode) { }
二、解题思路
2.1 常规思路
在单向链表中删除一个结点,最常规的做法无疑是从链表的头结点开始,顺序遍历查找要删除的结点,并在链表中删除该结点。这种思路由于需要顺序查找,时间复杂度自然就是O(n)。
2.2 正确思路
是不是一定需要得到被删除的结点的前一个结点呢?答案是否定的。
我们可以很方便地得到要删除的结点的一下结点。因此,我们可以把下一个结点的内容复制到需要删除的结点上覆盖原有的内容,再把下一个结点删除,就相当于把当前需要删除的结点删除了。
但是,还有两个特殊情况需要进行考虑:
(1)如果要删除的结点位于链表的尾部,那么它就没有下一个结点:
此时我们仍然从链表的头结点开始,顺序遍历得到该结点的前序结点,并完成删除操作,这仍然属于O(n)时间的操作。
(2)如果链表中只有一个结点,而我们又要删除链表的头结点(也是尾结点):
此时我们在删除结点之后,还需要把链表的头结点设置为NULL。
最后,通过综合最坏情况(尾节点需要顺序查找,1次)和最好情况(n-1次),因此平均时间复杂度为:
需要注意的是:受到O(1)时间的限制,我们不得不把确保结点在链表中的责任推给了函数DeleteNode的调用者。
三、解决问题
3.1 代码实现
public static void DeleteNode(Node<int> headNode, Node<int> deleteNode) { if (headNode == null || deleteNode == null) { return; } if (deleteNode.Next != null) // 链表有多个节点,要删除的不是尾节点:O(1)时间 { Node<int> tempNode = deleteNode.Next; deleteNode.Item = tempNode.Item; deleteNode.Next = tempNode.Next; tempNode = null; } else if (headNode == deleteNode) // 链表只有一个结点,删除头结点(也是尾结点):O(1)时间 { deleteNode = null; headNode = null; } else // 链表有多个节点,要删除的是尾节点:O(n)时间 { Node<int> tempNode = headNode; while(tempNode.Next != deleteNode) { tempNode = tempNode.Next; } tempNode.Next = null; deleteNode = null; } }
3.2 单元测试
(1)封装返回结果
该方法作为单元测试的对比方法,主要用来对比实际值与期望值是否一致:
public static string GetPrintNodes(Node<int> headNode) { if (headNode == null) { return string.Empty; } StringBuilder sbNodes = new StringBuilder(); while(headNode != null) { sbNodes.Append(headNode.Item); headNode = headNode.Next; } return sbNodes.ToString(); }
(2)测试用例
// 链表中有多个结点,删除中间的结点 [TestMethod] public void DeleteNodeTest1() { Node<int> head1 = new Node<int>(1); Node<int> head2 = new Node<int>(2); Node<int> head3 = new Node<int>(3); Node<int> head4 = new Node<int>(4); Node<int> head5 = new Node<int>(5); head1.Next = head2; head2.Next = head3; head3.Next = head4; head4.Next = head5; Program.DeleteNode(head1, head3); Assert.AreEqual(Program.GetPrintNodes(head1),"1245"); } // 链表中有多个结点,删除尾结点 [TestMethod] public void DeleteNodeTest2() { Node<int> head1 = new Node<int>(1); Node<int> head2 = new Node<int>(2); Node<int> head3 = new Node<int>(3); Node<int> head4 = new Node<int>(4); Node<int> head5 = new Node<int>(5); head1.Next = head2; head2.Next = head3; head3.Next = head4; head4.Next = head5; Program.DeleteNode(head1, head5); Assert.AreEqual(Program.GetPrintNodes(head1), "1234"); } // 链表中有多个结点,删除头结点 [TestMethod] public void DeleteNodeTest3() { Node<int> head1 = new Node<int>(1); Node<int> head2 = new Node<int>(2); Node<int> head3 = new Node<int>(3); Node<int> head4 = new Node<int>(4); Node<int> head5 = new Node<int>(5); head1.Next = head2; head2.Next = head3; head3.Next = head4; head4.Next = head5; Program.DeleteNode(head1, head1); Assert.AreEqual(Program.GetPrintNodes(head1), "2345"); } // 链表中只有一个结点,删除头结点 [TestMethod] public void DeleteNodeTest4() { Node<int> head1 = new Node<int>(1); Program.DeleteNode(head1, head1); head1 = null; Assert.AreEqual(Program.GetPrintNodes(head1), ""); } // 链表为空 [TestMethod] public void DeleteNodeTest5() { Program.DeleteNode(null, null); Assert.AreEqual(Program.GetPrintNodes(null), ""); }
测试通过结果:
(3)代码覆盖率
正文到此结束