(转)问题:假设一个没有头指针的单链表。一个指针指向此单链表中间的一个节点(既不是第一个,也不是最后一个节点),请将该节点从单链表中删除。

本文介绍如何在O(1)时间内删除单链表中的指定节点,包括无头指针链表中间节点及有头指针链表任意节点的删除方法。

问题:假设一个没有头指针的单链表。一个指针指向此单链表中间的一个节点(既不是第一个,也不是最后一个节点),请将该节点从单链表中删除。

p->data = p->next->data;

p->next = p->next->next;

firee(p->next); 

 

链表结点定义如下:

struct ListNode

{

         int       m_nKey;

         ListNode* m_pNext;

};

解答:假设给定的指针为pCurrent,ListNode* pNext = pCurrent->m_pNext;

由题意知,pCurrent指向链表的某一个中间节点,因此pCurrent->m_pNext != NULL。

要删除pCurrent指向的节点B很简单,但必须将节点B前后两个节点A和C连接起来,但是单链表节点没有头指针,因此无法追溯到A,也就无法将A和C相连了。

无法删除节点B,但我们可以删除B的后继节点C,并通过pCurrent->m_pNext = pCurrent->m_pNext->m_pNext重新将链表连接起来,而唯一丢失的是节点C的数据项m_nKey。因此,我们只需要将节点C的数据项取代节点B的数据项,然后将真正指向节点C的指针删除即可实现将节点B“删除”!!关键代码如下:

pCurrent->m_pNext = pNext->m_pNext;

pCurrent->m_nKey = pNext->m_nKey;

delete pNext;

完整代码如下:

#include <iostream>

#include <assert.h>

struct ListNode

{

    int m_nKey;

    ListNode* m_pNext;      

};

//参数必须是指向指针的指针,因为要修改指针

//所以要考虑“参数本地拷贝”问题

void InitList(ListNode** pList)

{

     *pList = (ListNode*)malloc(sizeof(ListNode));

     (*pList)->m_pNext = NULL;

}

//第一个参数不必是指向指针的指针,因为函数内只修改指针指向的数据

//所以不用担心“参数本地拷贝”问题

void InsertList(ListNode* pList, int data)

{

    ListNode* pNewNode = (ListNode*)malloc(sizeof(ListNode));

    pNewNode->m_nKey = data;

    pNewNode->m_pNext = pList->m_pNext;

    pList->m_pNext = pNewNode;

}

//核心算法

void DeleteRandomNode(ListNode* pCurrent)

{

    assert(pCurrent != NULL);

    ListNode* pNext = pCurrent->m_pNext;

    if(pNext != NULL)

    {

        pCurrent->m_pNext = pNext->m_pNext;

        pCurrent->m_nKey = pNext->m_nKey;

        delete pNext;      

                   pNext = NULL;

    }    

}

//打印链表元素

void PrintListNormally(ListNode* pListHead)

{

    ListNode* pTempNode = pListHead->m_pNext;

    while(pTempNode != NULL)

    {

        std::cout<<pTempNode->m_nKey<<std::endl;

        pTempNode = pTempNode->m_pNext;            

    }    

}

int main()

{

    ListNode* pHead = NULL;

    InitList(&pHead);

    for(int i=9; i>=0; i--)

    {

        InsertList(pHead,i);       

    }

    PrintListNormally(pHead);

    ListNode* pNext = pHead->m_pNext->m_pNext; //删除第个元素

    DeleteRandomNode(pNext);

    PrintListNormally(pHead);

    system("pause");

    return 0;   

}

=======================================================

扩展题目:给定链表的头指针和一个结点指针,在O(1)时间删除该结点。链表结点的定义如下:

struct ListNode

{

         int        m_nKey;

         ListNode*  m_pNext;

};

函数的声明如下:

void DeleteNode(ListNode* pListHead, ListNode* pToBeDeleted);

分析:这是一道广为流传的Google面试题,能有效考察我们的编程基本功,还能考察我们的反应速度,更重要的是,还能考察我们对时间复杂度的理解。

在链表中删除一个结点,最常规的做法是从链表的头结点开始,顺序查找要删除的结点,找到之后再删除。由于需要顺序查找,时间复杂度自然就是O(n) 了。

我们之所以需要从头结点开始查找要删除的结点,是因为我们需要得到要删除的结点的前面一个结点。我们试着换一种思路。我们可以从给定的结点得到它的下一个结点。这个时候我们实际删除的是它的下一个结点,由于我们已经得到实际删除的结点的前面一个结点,因此完全是可以实现的。当然,在删除之前,我们需要需要把给定的结点的下一个结点的数据拷贝到给定的结点中。此时,时间复杂度为O(1)。

上面的思路还有一个问题:如果删除的结点位于链表的尾部,没有下一个结点,怎么办?我们仍然从链表的头结点开始,顺便遍历得到给定结点的前序结点,并完成删除操作。这个时候时间复杂度是O(n)。

那题目要求我们需要在O(1)时间完成删除操作,我们的算法是不是不符合要求?实际上,假设链表总共有n个结点,我们的算法在n-1总情况下时间复杂度是O(1),只有当给定的结点处于链表末尾的时候,时间复杂度为O(n)。那么平均时间复杂度[(n-1)*O(1)+O(n)]/n,仍然为O(1)。

基于前面的分析,我们不难写出下面的代码。

核心参考代码:

//核心算法

//pListHead是链表头指针,pCurrent是要删除的节点指针

void DeleteRandomNode(ListNode* pListHead, ListNode* pCurrent)

{

    assert(pCurrent != NULL || pListHead != NULL);

    if(pCurrent->m_pNext != NULL) //要删除的节点不是最后一个节点

    {

        ListNode* pNext = pCurrent->m_pNext;

        pCurrent->m_nKey = pNext->m_nKey;

        pCurrent->m_pNext = pNext->m_pNext;

        delete pNext;

        pNext = NULL;                   

    }

    else //要删除的节点是链表中最后一个节点

    {

        ListNode* pNode = pListHead;

        while(pNode->m_pNext != pCurrent) //得到要删除节点的前继节点

        {

            pNode = pNode->m_pNext;                    

        }

        pNode->m_pNext = NULL;

        delete pCurrent;

        pCurrent = NULL;

    }

}

完整的代码只需替换第一题的相应函数,并在main函数调用中做相应修改即可。

值得注意的是,为了让代码看起来简洁一些,上面的代码基于两个假设:(1)给定的结点的确在链表中;(2)给定的要删除的结点不是链表的头结点。不考虑第一个假设对代码的鲁棒性是有影响的。至于第二个假设,当整个列表只有一个结点时,代码会有问题。但这个假设不算很过分,因为在有些链表的实现中,会创建一个虚拟的链表头,并不是一个实际的链表结点。这样要删除的结点就不可能是链表的头结点了。当然,在面试中,我们可以把这些假设和面试官交流。这样,面试官还是会觉得我们考虑问题很周到的。

本文来自优快云博客,转载请标明出处:http://blog.csdn.NET/ACE1985/archive/2010/08/06/5793287.aspx


<think>我们正在处理一个单链表(带节点)的插入操作。目标是在第i个节点前插入一个元素X。根据引用[1]和引用[3]的内容,我们知道:-单链表通常有一个节点(root),这样方便操作。-插入操作需要找到要插入位置的前一个节点。注意:题目要求在第i个节点前插入,也就是说,如果我们要插入到第i个位置(即成为链表的第i个节点),那么实际上是在第i-1个节点之后插入。但是,如果i=1,表示要在第一个节点前插入,也就是插入后成为新的第一个节点。由于我们有节点第一个节点实际上是节点的下一个节点。因此,我们可以这样设计:1.首先,我们需要检查插入位置i是否合法(i应该大于0且小于等于链表长度+1?注意:我们是在第i个节点前插入,所以i的范围应该是[1,size+1])。-当i=1时,表示在第一个节点前插入,即节点之后插入。-当i=size+1时,表示在最后一个节点之后插入(也就是在末尾添加)。2.然后,我们从节点开始遍历,找到第i-1个节点(即要插入位置的前一个节点)。注意:因为节点是第0个节点(不存储数据),所以我们要找的是第i-1个节点。3.创建新节点,并将新节点的next指向第i-1个节点的下一个节点(即原来的第i个节点)。4.将第i-1个节点的next指向节点。但是,这里有一个特殊情况:如果链表为空(只有节点),那么当i=1时,插入是合法的(插入后链表有一个数据节点);当i>1时,则不合法。另外,我们还需要注意链表长度的更新。根据上述分析,我们来实现insert方法:假设我们有一个单链表类,其中:-内部类Node,包含数据域data和指针域next。-节点head(不存储数据,第一个数据节点是head.next)-可能还有size记录节点数(数据节点个数)方法签名:publicvoidinsert(inti,Ex)步骤:1.检查i的合法性:i必须大于等于1且小于等于size+1(因为可以在最后一个节点之后插入,即i=size+1)。2.从节点开始,移动到第i-1个位置(即要插入位置的前驱节点)。我们用一个指针current,初始指向节点(位置0),然后移动i-1次。3.创建新节点,其数据为x。4.将新节点的next指向current.next。5.将current.next指向节点。6.更新size(如果维护了size)。注意:如果i=1,那么current就是节点,插入在节点后面,这是正确的。如果i=size+1,那么current会移动到最后一个节点,然后在最后一个节点后面插入,即新节点成为最后一个节点。代码实现如下:</think>### Java单链表(带节点)在第i个节点前插入元素X的实现 以下是完整的Java代码实现,基于带节点单链表结构,实现了在指定位置插入元素的功能: ```java // 定义链表节点类 class Node<T> { T data; // 节点数据 Node<T> next; // 指向一个节点指针 public Node(T data) { this.data = data; this.next = null; } } // 定义带节点单链表类 class LinkedList<T> { private Node<T> head; // 节点(不存储实际数据) private int size; // 链表长度 public LinkedList() { head = new Node<>(null); // 创建空节点 size = 0; } /** * 在第i个节点前插入元素X * @param index 插入位置(从1开始计数) * @param data 要插入的数据 * @throws IndexOutOfBoundsException 如果位置非法 */ public void insert(int index, T data) { // 验证位置有效性(1 ≤ i ≤ size+1) if (index < 1 || index > size + 1) { throw new IndexOutOfBoundsException("插入位置无效: " + index); } Node<T> newNode = new Node<>(data); Node<T> current = head; // 从节点开始遍历 // 移动到插入位置的前驱节点(第i-1个节点) for (int pos = 0; pos < index - 1; pos++) { current = current.next; } // 执行插入操作 newNode.next = current.next; // 新节点指向原位置节点 current.next = newNode; // 前驱节点指向节点 size++; // 更新链表长度 } // 其他辅助方法(可选) public void printList() { Node<T> current = head.next; while (current != null) { System.out.print(current.data + " -> "); current = current.next; } System.out.println("null"); } } // 使用示例 public class Main { public static void main(String[] args) { LinkedList<String> list = new LinkedList<>(); // 在位置1插入(链表为空时相当于首插) list.insert(1, "A"); // 在位置1插入(成为新首节点) list.insert(1, "B"); // 在位置3插入(相当于尾插) list.insert(3, "C"); list.printList(); // 输出: B -> A -> C -> null } } ``` ### 关键实现说明: 1. **节点作用**:节点(`head`)不存储实际数据,其`next`指向第一个数据节点,统一了首节点和其他节点的插入操作逻辑[^1][^3] 2. **位置参数**: - `index=1` 表示在第一个数据节点前插入(成为新首节点) - `index=size+1` 表示在链表末尾插入 3. **插入流程**: - 遍历到位置`i-1`的节点(前驱节点) - 新节点`next`指向原`i`位置节点 - 前驱节点`next`指向节点 4. **边界处理**: - 首节点插入:`index=1`,前驱节点节点 - 尾节点插入:`index=size+1`,前驱节点即尾节点 - 空链表插入:自动成为首节点 ### 时间复杂度: - **最优**:$O(1)$(插入到链表部) - **最差**:$O(n)$(插入到链表尾部) - **平均**:$O(n)$ 此实现严格遵循了单链表在指定位置前插入元素的规范要求[^3],通过节点简化了首节点操作的特殊处理,确保了操作一致性。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值