问题:
给定一个单向链表,设计一个时间优化并且空间优化的算法,找出该链表的倒数第m个元素。实现您的算法,注意处理相关的出错情况。m定义为当m=0时,返回链表最后一个元素。
解答:
这是一个难题,因为单向链表只能正向遍历,这个问题需要根据元素与链表尾的相对位置来找出该元素,但是当发现链表尾时,没有简单的办法回溯到倒数第m个元素。
我们需要的是倒数第m个元素,所以,如果我们从某个元素开始,遍历了m个元素之后刚好到达链表末尾,那这个元素就是要找的元素。一种方法是简单的以这种方式检查每个元素,直到找到要找的元素为止。但这样同样的元素会被遍历多次,针对链表中大部分元素我们都会遍历m个元素,如果链表的长度是n的话,那么这个算法的时间复杂度就是O(mn)。
也许从链表的尾部倒推回去不是最好的办法,那么我们可以从链表头开始计数。
思路一:想要的元素是倒数第m个元素,而我们知道m值。它肯定是从链表头算起的第x个元素,且x+m=n,即链表的长度。而计算出链表的所有元素个数是容易的。之后就可以算出x=n-m,并从链表头开始遍历x个元素。尽管这个算法需要2次遍历链表,但它的时间复杂度仍是O(n)。同时,如果我们可以修改链表的函数,在添加元素时对一个计数遍历加1,在删除元素时对这个计数变量减1,我们就可以省掉链表长度计算的过程,使它更有效率。
上面思路的缺点是:如果这是一个很长的链表而计算机的内存又有限,那么这个链表的大部分很可能都存放在切换出物理内存的虚拟内存上(即存放在磁盘上)。如果真是这样,那么链表的每次遍历都将需要做大量的磁盘读写操作才能把链表的有关部分读到物理内存里来进行处理。在这种情况下,如果算法只需对链表做一次完整的遍历,那么它将比必须做两次遍历的算法快很多---虽然它们都是O(n)级的算法。
但是,如果不能修改链表的数据结构呢?在上面的分析中,当我们到达链表的末尾时,实际上只对保存下来的m个元素中的一个感兴趣,即当前位置的前面第m个元素。我们之所以记下m个元素只是因为当前位置的前面第m个元素在每次移动位置时都会改变。保持一个m个元素的队列,每次在移动当前位置时,将当前元素添加在头部,在尾部删除一个元素,这样做只是为了确保队列中最后一个元素总是当前位置的前面第m个元素。
思路二:透过想象看本质,上面我们使用这个m个元素的数据结构来隐式地移动一个前m个元素的指针,保持它与当前位置的指针同步移动。但是,这个数据结构是不必要的,我们可以显式地移动前面第m个元素的指针,就像移动当前位置的指针一样,这与通过一个队列隐式地移动同样简单,这样一来,就无需保存当前位置和前面第m个元素之间的所有元素。这个算法的优点是:线性时间复杂度、一次遍历、可以忽略的存储要求。
此时,我们需要使用两个指针:一个当前位置指针和一个前面第m个元素的指针。需要确保两个指针之间相差m个元素,然后以同样的速度移动它们。如果当前指针到达链表的末尾,前面第m个元素的指针就是指向倒数第m个元素。同时要注意边界条件,如果链表的长度小于m,那么就没有倒数第m个元素,因此,在移动当前指针时要检查是否到达链表的末尾。
实现代码如下:
#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)
{
assert(pList != NULL);
ListNode* pNewNode = (ListNode*)malloc(sizeof(ListNode));
pNewNode->m_nKey = data;
pNewNode->m_pNext = pList->m_pNext;
pList->m_pNext = pNewNode;
}
//思路一的核心算法
//注意:链表最后一个元素是倒数第0个
ListNode* findMLastElement1(ListNode* head,
unsigned
int m)
{
if(head == NULL)
return NULL;
//计算链表中节点数º
ListNode* pCurrent = head->m_pNext;
unsigned
int nCount = 0;
while(pCurrent != NULL)
{
pCurrent = pCurrent->m_pNext;
nCount++;
}
//如果链表中节点数少于m,返回NULL
if(nCount < m)
return NULL;
//链表倒数第m个节点就是从链表开头第nCount-m个节点
//注意:m从0开始
pCurrent = head;
for(unsigned
int i=0; i<nCount-m; i++)
{
pCurrent = pCurrent->m_pNext;
}
return pCurrent;
}
//思路二的核心算法
//注意:链表最后一个元素是倒数第个
ListNode* findMLastElement2(ListNode* head,
unsigned
int m)
{
if(head == NULL)
return NULL;
ListNode *current, *mBehind;
current = head;
for(int
i=0; i<m; i++)
{
//current设置为当前指针,它前面必须存在第m个元素,否则出错
if(current->m_pNext != NULL)
{
current = current->m_pNext;
}
else
{
return NULL;
}
}
mBehind = head; //当前位置current的前面第m个元素的指针,从链表头开始移动
while(current->m_pNext != NULL)
{
current = current->m_pNext;
mBehind = mBehind->m_pNext;
}
return mBehind;
}
//打印链表元素
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* pListHead = NULL;
InitList(&pListHead);
for(int
i=9; i>=0; i--)
{
InsertList(pListHead, i);
}
PrintListNormally(pListHead);
ListNode* findNode = NULL;
findNode = findMLastElement1(pListHead, 3);
std::cout<<"链表中倒数第3个元素是:"<<findNode->m_nKey<<std::endl;
findNode = findMLastElement2(pListHead, 4);
std::cout<<"链表中倒数第4个元素是:"<<findNode->m_nKey<<std::endl;
system("pause");
return 0;
}
扩展问题:
如何获得处于单链中间位置的节点(只能遍历链表一次!)
解答:
其实原理和上面的一样,就是设置两个指针,或游标(形象点):
Element *pSlow = head;//标识当前节点
Element *pFast = head;//标识当前节点的下一个节点
将慢游标前进步长设为1,快游标前进步长设为2:
pSlow = pSlow->next;//前进一个节点
pFast = pFast->next->next;//前进两个节点因此,快游标是慢游标速度的两倍,
//当快游标到达链表尾节点或尾节点前一个节点时,慢游标正好处于链表的
//中间位置,即为所求。
分享到:
相关推荐
总结一下,查找链表中倒数第k个节点的问题主要考察了对链表数据结构的理解和操作,以及双指针算法的应用。`MyLinkList.java`文件中的实现可能包括了链表节点和链表操作的封装,以及使用双指针解决特定问题的逻辑。...
总的来说,解决"仅遍历一次得到链表的倒数第n个结点"的问题,需要理解和运用链表的特性和双指针技巧,这是一种在数据结构和算法面试中常见的问题,能够很好地考察候选人的逻辑思维和编程能力。在实际的编程练习中,...
在这个问题中,我们需要处理一个特殊的链表操作,即“删除链表中倒数第N个节点”。这个任务在C++编程中尤为常见,因为它涉及到基本的指针操作和算法设计。 链表由一系列节点组成,每个节点包含数据和指向下一个节点...
处理单链表的问题时,有时我们需要找到链表中的特定位置的元素,例如“取单链表倒数第k个元素”这个问题。这个问题在面试中经常出现,因为它能够考察开发者对链表操作的理解和编程能力。 ### 算法描述 要找到...
删除链表中倒数第N个节点的问题,可以通过以上两种不同的思路解决。解题思路一在逻辑上更直接简单,适合初学者理解和实现,但消耗额外空间;而解题思路二更为高效,避免了额外的空间开销,是空间复杂度上的优化方案...
在这个问题中,我们需要找到链表中倒数第 k 个节点。这个问题是LeetCode上的一个经典题目,涉及到链表操作和算法设计。 首先,让我们定义链表节点的结构体: ```cpp struct ListNode { int val; ListNode *next;...
总的来说,这个C语言程序实现了链表中倒数第k个节点的查找功能,通过遍历和双指针技巧有效地解决了问题。在实际应用中,这种技巧经常用于处理链表相关的数据结构问题,比如寻找中点节点或删除特定位置的节点等。理解...
在数组中,元素是连续存储的,而链表则通过指针连接各个节点,使得元素可以在内存中的任意位置分布。这种数据结构在处理动态变化的数据集合时特别有用,因为它允许在不预先知道数据规模的情况下高效地进行插入和删除...
17. **删除倒数第k个节点**:给定一个整数k,删除链表的倒数第k个节点。 18. **链表的环的起始节点**:如果链表有环,找到环的起始节点。 19. **单链表的反转K个节点**:每次反转链表中的连续K个节点,保持其他...
本题解集中于LeetCode的第19题,即“删除链表的倒数第N个节点”。这道题目主要涉及链表操作,特别是涉及到链表的遍历和节点删除,是链表数据结构的基础应用之一。 首先,我们需要理解链表的基本概念。链表是一种...
对于给定的问题“求链式线性表的倒数第K项”,我们需要设计一个算法,能够在链表中找到最后一个元素之后的第K个元素。 首先,我们需要理解链表的基本操作,如创建、插入、删除节点。在C语言中,链表节点的结构体...
2. **找出单链表的倒数第4个元素** 这通常通过两次遍历来完成:第一次遍历获取链表长度,第二次遍历至特定位置(长度减去3)。更高效的方法是使用双指针技术,其中一个指针先向前移动三步,然后两个指针同步移动,...
第19题,即“删除链表的倒数第N个节点”,是链表类问题中一个经典题目,旨在考察程序员对链表操作的理解以及如何在不遍历整个链表的情况下找到目标节点。 链表是一种数据结构,它由一系列节点组成,每个节点包含...
在本压缩包中,我们关注的是C#编程语言在解决LeetCode算法问题中的应用,特别是第19题——"删除链表的倒数第N个节点"。这是一道典型的链表操作题目,旨在考察程序员对链表结构的理解以及如何在单链表中进行高效的...
本压缩包文件"python_leetcode面试题解之删除链表的倒数第N个节点.zip"聚焦于解决一个具体的LeetCode问题,即如何在链表中删除倒数第N个节点。 链表是一种基础的数据结构,由一系列节点构成,每个节点包含数据和...
第19题,删除链表的倒数第N个节点,是一个典型的链表操作问题,对于求职者来说,掌握这类问题的解决方案至关重要。这道题目旨在考察候选人对链表的基本操作、指针技巧以及对数据结构的理解。 首先,我们需要了解...
在探索单链表数据结构的过程中,一个常见且具有挑战性的问题是如何高效地查找倒数第n个元素。由于链表的特性,其元素只能单向遍历,这使得直接查找成为一项复杂任务。然而,巧妙地运用双指针技术,我们能够在一次...
链表中倒数第k个节点.md
3. **找到链表的倒数第n个元素**: 这个算法使用两个指针`p1`和`p2`,它们都从头结点开始。先让`p2`移动`n`步,然后`p1`和`p2`同时移动,直到`p2`到达链表末尾。此时,`p1`正好在倒数第`n`个元素的位置。 4. **两...