单向链表的常见问题

链表中的对象也是按线性顺序排列的,但与数组不同,数组的线性顺序是由数组的下标,再深层次说是由数组的内存结构限定的,而链表中的顺序则是由各对象中的指针决定的。

声明一个单向链表如下:

class SLink_ListNode
{
public:
SLink_ListNode(int data,SLink_ListNode* pNext = NULL);
~SLink_ListNode();
void ShowData();

int m_data;
SLink_ListNode* m_next;
};

class SingleLink_List
{
public:
SingleLink_List();
~SingleLink_List();

SLink_ListNode* Append(int data);//添加一个结点并设置数据
int Delete(SLink_ListNode* pNode);//删除指定结点
SLink_ListNode* Search(int data);//查询指定数据的结点
int Delete(int data);//删除指定数据的结点
void ShowList();//显示列表数据
void Reverse();//非递归方法的列表反转
void RecursiveReverse(SLink_ListNode* pHead);//递归方法的列表反转
bool HasRing();//检测一个单向链表是否存在一个环
SLink_ListNode* FirstRingNode();

SLink_ListNode* m_pHead;
int m_Length;
};


 

出于个人长久记忆的原因,逐一对每一个函数的实现做一下说明,毕竟最近面试遭人鄙视了,TMD!

//链表结点的内部成员虽有指针类型,但此处无需进行内存操作,因为我们就是只需要一个指针变量,用于指向未来想指向的玩意。
SLink_ListNode::SLink_ListNode(int data,SLink_ListNode* pNext)
{
m_data = data;
m_next = pNext;
}

SLink_ListNode::~SLink_ListNode()
{
m_data = 0;
m_next = NULL;
}


此处在构造链表结点时,就将想存储的数据进行存储,个人感觉“做女人,这样挺好”。

下面是LIST的构造与析构:

SingleLink_List::SingleLink_List()
{
//对于起始的一个空LIST而言,指针设置为空,长度设置为0就好了
m_pHead = NULL;
m_Length = 0;
}

SingleLink_List::~SingleLink_List()
{
SLink_ListNode* pNext = m_pHead;
//依次向后推移头结点指针,推移一次,删除一个
while(pNext)
{
m_pHead = pNext->m_next;
delete pNext;
pNext = m_pHead;
}
m_Length = 0;
}


下面是链表的扩充方法:

SLink_ListNode* SingleLink_List::Append(int data)
{
//向LIST中增加结点,首先要做的就是先为新结点开辟一块内存
SLink_ListNode* pNode = new SLink_ListNode(data);
if(NULL == pNode){
return NULL;
}
//结点生成之后,从LIST的前面逆向插入到链表中就可以了,这里有点栈的味道了,先插入的后被访问到
pNode->m_next = m_pHead;
m_pHead = pNode;

m_Length ++;

return m_pHead;
}


 下面是链表的结点删除方法:

//删除方法分了三种情况,首先看是不是个空链表,其次看是不是删除的头结点,最后才是删除的非头结点
//因为头结点是没有前驱结点的,所以鄙人专门将删除头结点的情况专门处理
int SingleLink_List::Delete(SLink_ListNode* pNode)
{
if(NULL == pNode){ //
return -1;
}

if(pNode == m_pHead){//删除头结点,简单改变下头指针的指向就可以删除了
m_pHead = m_pHead->m_next;
delete pNode;
m_Length --;

return 0;
}

//删除非头结点,涉及到前,中,后三个结点的链接问题
SLink_ListNode *pNext = m_pHead->m_next;
SLink_ListNode *pPrev = m_pHead;

while(pNext)
{
if(pNode == pNext){
pPrev->m_next = pNode->m_next;
delete pNode;

m_Length --;

return 0;
}else{
pPrev = pNext;
pNext = pNext->m_next;
}
}

return -1;//链表中不存在要删除的结点,没办法
}


 查询方法:

//链表中数据的查询,只是一个简单的链表遍历
SLink_ListNode* SingleLink_List::Search(int data)
{
SLink_ListNode* pNode = m_pHead;

while(NULL != pNode && pNode->m_data != data)
{
pNode = pNode->m_next;
}

return pNode;
}

int SingleLink_List::Delete(int data)
{
SLink_ListNode* pDelete = Search(data);

return Delete(pDelete);
}


单向链表就是个独眼龙,只能看到一个方向,这有好处也有坏处,好处是人家单纯,人家只看一个方向,坏处是他有点死板,只会正着搞,反着就不会。

也正是这个特点,使单向链表的反转成为一个经典的笔试题目:

先上代码:

void SingleLink_List::Reverse()
{

if(NULL == m_pHead)
{
return;
}

//从链表的第一个结点开始摸
SLink_ListNode* pCurrent = m_pHead;
//事先先摸清楚链表的第二个结点
SLink_ListNode* pNext = pCurrent->m_next;
//这里再找一个中介,因为你一旦把“线”给剪断了,如果不靠别的玩意临时抓住它的兄弟们,你就永远失去它们了
SLink_ListNode* pReverse = NULL;
//链表反转常忘记的问题,那就是开始的头结点,你丫的本来“前无古人”,现在你要“后无来者”了。
pCurrent->m_next = NULL;

while(NULL != pNext)//这里,循环的条件就变成“有货,咱就给让它转向”
{
pReverse = pNext->m_next;//抓住它的兄弟们!!务必
pNext->m_next = pCurrent;//给它本人转向
pCurrent = pNext;//更新转向灯,指定下一个转向点
pNext = pReverse;//让它的兄弟们一个一个转向!
}

m_pHead = pCurrent;//最后一个了,让它变成“老大”就是了
}

反转的基本思想就是:

先让以前的头变尾(即将其NEXT指针设置为NULL),然后依次将每一个元素作为转向灯(即让其后的元素的NEXT指针指向它),从第二个元素开始让其转向,期间要注意一定要在切断“绳子”之前,

先想办法抓住它的兄弟们(即找一个临时指针指向当前要转头的结点的后续列表),转完一个之后,要立即更新转向灯,依次进行,如是而已。 

 

采用递归实现的话,方式微微有些变化,因为它是采用的从后向前转向的方式,不解释,直接上代码:

void SingleLink_List::RecursiveReverse(SLink_ListNode* pHead)
{
if(NULL == pHead){
return;
}

if(NULL == pHead->m_next){
//当前结点已是最后一个,则让链表的头指针指向它就可以了
m_pHead = pHead;
}else{
//如果不是最后一个,则先解决它的内部问题,这也就是递归的思想了,经此步,一定会走进if条件中,
//完成链表新头结点的定位
RecursiveReverse(pHead->m_next);
//代码第一次走到这里,一定是经过一次回溯之后到达了原链表的倒数第二个结点
//此时的pHead就是倒数第二个结点,而它的NEXT是最后一个结点,再NEXT就是最后一个结点要指向的结点了
//好了,调个头就是了,让最后一个结点的的NEXT指向倒数第二个结点,就完成了调头

//一旦再次回溯,即RecursiveReverse再RETURN一次,pHead就是倒数第三个结点了,如是而已
pHead->m_next->m_next = pHead;
//这里有一个小小的细节,那就是在回溯的过程中,将每一个当前的结点的NEXT指定为NULL,这行代码虽然做了次重复的工作
//即为当前的结点指定后继,但这是必须的,这是为了保证原链表的头结点最终指向NULL,以结束链表
pHead->m_next = NULL;
}
}

 

如此说来,链表的转向,无非就这两种方法,因为单向链表的特性,决定了其操作(哪位要是有第三种方法,请一定要赐教!)


接下来,讨论另一个经典的问题,问,如何检测一个单向链表有没有环?

如果按照传统最笨的方法,第一感觉就是一一作比较,不过仔细想,单向链表一旦有环,意味着什么?

是不是意味着你永远也无法通过与NULL值的比较找到链表的结尾?此处暂时不多说,先看下流行且经典的检测单向链表是否存在环的方法:

 

bool SingleLink_List::HasRing()
{
if(NULL == m_pHead)
{
return false;
}

SLink_ListNode* pSlow = m_pHead;
SLink_ListNode* pFast = m_pHead;

while(NULL != pSlow && NULL != pFast && NULL != pFast->m_next)
{
pSlow = pSlow->m_next;//一次前进一个结点
pFast = pFast->m_next->m_next;//一次前进两个结点

if(pSlow == pFast){//重合了!意味着什么?
return true;
}
}

return false;
}

此函数的原理想必大家都知道,即从链表的头开始,指定两个指针,一个一次移动一个位置,另一个一次移动两个位置,只要二者在某一时刻相等了,就意味着这个链表是存在环的。

为什么?为什么出现相等的情况就意味着当前链表存在环?

此问题我也是纠结半天,看了网上了不少数学证明,基本不理解,不过在不经意间突然想到:

两个指针,A,B,从链表LIST的头结点开始依次向后走,且A移动的比B移动的慢。此时,作一个假设:假设当前链表不存在环,那么B的位置永远在A的前面,A是绝对在一万年的时间

内也不可能与B相等或超过B,直到链表的结束;但是,如果出现了二者相等的情况(当然A超过B这种情况肯定是正确,但是程序对这种情况是无法做出有效检测的,只有相等才是可以

准确检测出来的),意味着什么?在什么条件下A才有可能与B相等?想想学习里的直线与曲线,明白了吧~~,只有链表出现“贪吃蛇”失败的情况,才有这种可能。

由此可知,相等必定有环,有环必定会出现相等的情况。


继续下一话题:如果确定了单向链表存在环,那如何确定环的起始位置呢?

看了别人的总结,心中暗自一凉,不用数学计算,是不会产生这种方法的:

 

假设二个指针移动了n次之后相遇了,且相遇的位置距离环的起始位置的距离是M,则,这两个指针移动的距离是:

慢的:n = L + M + xC    (x是慢指针在相遇前在环内的移动次数)

快的:2*n = L + M + yC  (y是快指针在相遇前在环内的移动次数)

由此可以计算得出:2L + 2M + 2xC = L + M + yC

化简后得到:L = (y-2x)C - M

此公式意味着什么?

首先说x与y,因为x是慢指针在环内的移动次数,y是快指针在环内的移动次数,试想,慢的能跑过快的吗?所以x肯定比y小。

其次,二者肯定相遇,慢的移动一个结点,快的移动两个结点,相当于后者与前者的距离在以1为单位进行缩小,总会有赶上而不是跨越的那一点。

最后,假设慢指针在首次进行环的第一个结点的时间,快指针在距离环起始结点距离为M的位置,M的取值范围是【0,C-1】,即M肯定不会大于C,

如此一来,慢指针在绕了马上整整一圈的时间,快指针已经马上绕了快两圈,二者的差距已经被缩小了快整整一圈,而二者的差距最大就是C-1,

且由于“其次”中论证的二者必须是相遇,所以慢指针在未绕环一圈时,快指针肯定会追上它,所以这里的x=0,y=1。


再所以,L=C-M。而L的长度正是环的起始点的坐标,如是而已,代码如下:

SLink_ListNode* SingleLink_List::FirstRingNode()
{
if(NULL == m_pHead)//是否空链表
{
return NULL;
}

SLink_ListNode* pSlow = m_pHead;
SLink_ListNode* pFast = m_pHead;

while(NULL != pSlow && NULL != pFast && NULL != pFast->m_next)
{
pSlow = pSlow->m_next;
pFast = pFast->m_next->m_next;

if(pSlow == pFast){
break;
}
}

if(NULL == pFast){//是否没有环
return NULL;
}

pFast = m_pHead;
while(pSlow != pFast)
{//从头一次移动一个位置,由于二者此时距离环头结点的距离相等,
//所以检测出相等时,二者此时同时指向了链表环的头结点
pSlow = pSlow->m_next;
pFast = pFast->m_next;
}

return pSlow;
}


接下来再说下两个单向链接是否有交叉点。

仔细想一想,两个单向链表如果存在交叉点,那两个链表会组成一个什么形状?

对,只能是Y形。接下来,也正是利用这个Y形,对这种特殊关系的链表做了一些操作,先看如何检测两个单向链表是否存在交叉点:

bool ListsHaveCross(SingleLink_List s1,SingleLink_List s2)
{
if(s1.HasRing() || s2.HasRing())
{
//m*n的一一比较,比较时不要对NODE进行NULL检测,而是直接使用LIST的长度进行循环比较

return false;
}
else
{
SLink_ListNode* pNode1 = s1.m_pHead;
SLink_ListNode* pNode2 = s2.m_pHead;

while(NULL != pNode1){//获取LIST1的最后一个结点
pNode1 = pNode1->m_next;
}

while(NULL != pNode2){//获取LIST2的最后一个结点
pNode2 = pNode2->m_next;
}

return pNode1 == pNode2;
}
}

上函数中if并没有填写,因为如果有一单向链表存在环,那else中的逻辑便可能不再适用,因为如果事先不知道一个链表的长度,我们还需要一个辅助比较的过程去确定链表的长度,

所以我建议在写链表的时间,最好有一个记录其长度的字段,有了长度,一切都简单很多。

ELSE中代码的原理,就是利用了两个单向链表一旦交叉,其注定是一个Y形的原理,我们只需要去检测两个链表的最后一个元素是否相同,就可以做出两个链表是否交叉的判断。


那再接下来,如何确定两个交叉链表的第一个交叉结点呢?同样是利用其Y形的特性,代码如下:

 

SLink_ListNode* ListFirstCrossNode(SingleLink_List s1,SingleLink_List s2)
{
if(false == ListsHaveCross(s1,s2))
{
return NULL;
}
else
{
SLink_ListNode* pNode1 = s1.m_pHead;
SLink_ListNode* pNode2 = s2.m_pHead;
int iMoved = s1.m_Length - s2.m_Length;

//将长的链表从头先移动“长度差”个结点
if(iMoved > 0)
{
for(int i = 0;i < iMoved;++i)
{
pNode1 = pNode1->m_next;
}
}
else if(iMoved < 0)
{
for(int i = 0;i < abs(iMoved);++i)
{
pNode2 = pNode2->m_next;
}
}

//在对应位置一一比较
while(pNode1 != pNode2){
pNode1 = pNode1->m_next;
pNode2 = pNode2->m_next;
}

return pNode1;
}
}

该函数的原理也是相当简单,我们只需要将Y的两个翅膀先做成一样的长度,然后同时向后走并做比较,第一次出现相同的结点,就是首次的交叉结点。

如是而已~~


好了,意向链表大致就这些考点了,以上代码全是个人书写,肯定会存在问题,还请发现问题的朋友们多多赐教!!

本人喜欢玩WOW,附个人PP一张,哈哈

 

拜拜~~


posted @ 2012-02-18 14:06  Marstar  阅读(2208)  评论(0编辑  收藏  举报