为了方便演示,我们简单的定义几个单链表的操作:
typedef int DataType;
typedef struct LinkListNode
{
DataType _data;
struct LinkListNode* _next;
}Node,*pNode,*pList;
void InitList(pList* pplist)
{
assert(pplist);
*pplist = NULL;//头指针置空
}
pNode BuyNode(DataType x)
{
pNode newNode = (pNode)malloc(sizeof(Node));
if (newNode == NULL)
{
perror("BuyNode::malloc");
exit(EXIT_FAILURE);
}
newNode->_data = x;
newNode->_next = NULL;
return newNode;
}
void PushBack(pList* pplist,DataType x)
{
pNode newNode = BuyNode(x);
pNode cur = *pplist;
assert(pplist);
//链表为空,新插入节点为链表头结点
if (cur == NULL)
{
*pplist = newNode;
}
else
{
while (cur->_next != NULL)
{
cur = cur->_next;
}//cur->_next==NULL;
cur->_next = newNode;
}
}
void PopBack(pList* pplist)
{
pNode cur = *pplist;
//如果链表为空,直接返回
assert(pplist);
if (cur == NULL)
{
return;
}//只有一个节点
if (cur->_next == NULL)
{
free(*pplist);
*pplist = NULL;
}
else
{
pNode prev = cur;
cur = cur->_next;
while (cur->_next != NULL)
{
cur = cur->_next;
prev = prev->_next;
}//cur->_next==NULL;
free(cur);
cur = NULL;
prev->_next = NULL;
}
}
1.逆序打印单链表
void ReversePrint(pList plist)
- 思路:我们要打印单链表就必须遍历整个链表,逆序打印即先遍历的节点后打印,后遍历的节点先打印。
- 方法:我们可以采用递归的方式,每遍历一次,打印最后的节点,并且将它Pop掉,直到节点为空。
- 实现如下:
void ReversePrint(pList plist)
{
pList cur = plist;
if (cur == NULL)
{
printf("over\n\n");
return;
}
while (cur->_next != NULL)
{
cur = cur->_next;
}
printf("%d -> ", cur->_data);
PopBack(&plist);
ReversePrint(plist);
}
2.删除无头单链表的非尾节点
void EraseNotTail(pNode pos);
- 思路:首先,这个pos位置可能是头结点,也可能是中间节点。常规思路是:定义一个前驱节点,然后找到pos位置然后删除(但是如果定义前驱指针的话我们必须知道链表的头在哪里);第二种思路是:如果我们只知道pos位置,将pos位置的值与下一个节点的值进行交换,然后删除pos的下一个位置,这个方法不用定义前驱指针。
- 实现如下:
void EraseNotTail(pNode pos)
{
//若pos为尾节点,则直接返回
if (pos->_next == NULL)
{
return;
}
DataType tmp;
tmp = pos->_data;
pos->_data = pos->_next->_data;
pos->_next->_data = tmp;
pNode del = pos->_next;
pos->_next = pos->_next->_next;
free(del);
del = NULL;
}
3.在无头单链表的非头结点前插入一个元素
void InsertFrontNode(pNode pos, DataType x);
- 思路:没有头指针,无法通过设置前驱指针来插入数据。将新节点插入到pos位置之后,然后交换新节点和pos
- 实现如下:
void InsertFrontNode(pNode pos, DataType x)
{
if (pos->_next == NULL)
{
return;
}
pNode newNode = BuyNode(x);
newNode->_next = pos->_next;
pos->_next = newNode;
DataType tmp;
tmp = pos->_data;
pos->_data = newNode->_data;
newNode->_data = tmp;
}
4.约瑟夫环问题
void JosephCycle(pList* pplist, int k);
- 约瑟夫环问题:已知n个人(以编号0,1,…,n-1分别表示)围坐在一张圆桌周围。从编号为k的人开始报数,数到m的那个人出列;他的下一个人又从1开始报数,数到m的那个人又出列;依此规律重复下去,直到圆桌周围的人全部出列。
- 我们要解决的约瑟夫环问题是:有n个人,从下标为1的人开始报数,报到k的人出列,下一个继续从1开始报数,……,重复上述过程,直到最后一个人出列,我们要求的就是最后一个出列的人的位置
思路:
- 首先,我们分析当n变化时,k=2时,J(k=)(n=)=?的场景:
n=2时,J2(2)=1;
n=3时,J2(3)=3;
n=4时,J2(4)=1;
n=5时,J2(5)=3;
n=6时,J2(6)=5;
n=7时,J2(7)=7;
n=8时,J2(8)=1;
…… - 通过上面简单的列举,我们很容易可以知道,J2(2^x)=1(x为2的幂);知道这一规律后我们再来分析一下剩下的这些不是2的幂的n对应的结果:n=3时,当出局一人后,又回到了n=2时的场景,只不过下标发生了变化;同样n=5,n=9时,当出局一人后,又回到了n=4,n=8时的场景,我们分别来看一下这两种情况下的坐标是如何发生变化的:
- 我们可以得出大致的结论:即new=(old-2)%n ( 姑且认为-1%5=4、-1%9=8)
- 下面我们考虑一般情况:k为任意数时,我们也可以通过上面的方法来考虑,J(k)(n+1)可以通过J(k)(n)来得到;我们随便举一个例子,假设k=5,n=9,我们来观察一下它的坐标变化是否符合我们上边说的规律:
- 很明显,新的坐标的变化规律是符合我们上面根据特例推出来的规律的,即new=(old-k)%n ==》 old=(new+k)%n。根据前面的思想:J(k)(n+1)可以通过J(k)(n)来得到,J(k)(n)可以通过J(k)(n-1)得到,这是一个不断递归的过程。总结一下:
J(k)(1)
J(k)(2)=(J(k)(1)+k)%n
J(k)(3)=(J(k)(2)+k)%n
……
J(k)(n)=(J(k)(n-1)+k)%n - 所以,我们最终推导出来的约瑟夫环问题的通解为:J(k)(n)=(J(k)(n-1)+k)%n
- 代码实现:
int _JosephCycle(int n, int k)
{
if (n == 1)
return 1;
else
return (_JosephCycle(n - 1, k) + k) % n;
}
int JosephCycle(pList* pplist, int k)
{
int n = 0;
pNode cur = *pplist;
assert(pplist);
while (cur)
{
n++;
cur = cur->_next;
}
return _JosephCycle(n, k);
}
5.逆序单向链表
void ReverseList(pList* pplist);
- 思路:如果当前链表为空或者只有一个节点,则不需要处理;否则,将原链表的的每个节点从头开始依次摘下来,将其通过头插法链在以prev为头结点的逆置链表中,每次摘节点之前都要并保存好下一个节点的位置,在处理的过程中,关注第一个节点和最后一个节点的特殊性,整个链表完成逆置后,将prev赋值给头指针*pplist,整个链表就完成了逆置
- 图示上述过程:
- 实现如下:
void ReverseList(pList* pplist)
{
pNode cur = *pplist;
assert(pplist);
if (cur == NULL || cur->_next == NULL)
{
return;
}
else
{
pNode tmp = cur->_next;
cur->_next = NULL;
pNode prev = *pplist;
while (tmp)
{
prev = cur;
cur = tmp;
tmp = tmp->_next;
cur->_next = prev;
}//tmp==NULL,即cur为最后一个处理节点
cur->_next = prev;
prev = cur;
*pplist = prev;
}
}
6.合并两个有序链表,使之合并后依然有序
pList Merge(pList* p1, pList* p2);
思路:假定是将两个升序链表合成一个升序链表(若是两个升序合成一个降序,则只需将链表逆置即可),比较两个两链表的首节点,将值小的那个做为新链表的头节点,依次比较两个链表中的节点,将小的那个链在新链表上,如果两个链表不等长,则必有一个先遍历完,此时只需将剩余元素链在新链表之后即可
实现如下:
pList Merge(pList* p1, pList* p2)
{
pNode pnew = NULL;
pNode pcur = NULL;
pNode h1 = *p1;
pNode h2 = *p2;
if (h1 == NULL)
return h1;
if (h2 == NULL)
return h2;
if ((*p1)->_data < (*p2)->_data)
{
pnew = *p1;
h1 = h1 ->_next;
}
else
{
pnew = *p2;
h2 = h2 ->_next;
}
pcur = pnew;//保存新链表的头
while (h1 != NULL && h2 != NULL)
{
if (h1->_data <= h2->_data)
{
pcur->_next = h1;
pcur = pcur->_next;
h1 = h1->_next;
}
else
{
pcur->_next = h2;
pcur = pcur->_next;
h2 = h2->_next;
}
}
//h1==NULL
if (h1 == NULL)
{
pcur->_next = h2;
}
//或者 h2=NULL
if (h2 == NULL)
{
pcur->_next = h1;
}
return pnew;
}
7.查找单链表的中间节点,要求只能遍历一次链表
pNode FindMidNode(pList plist);
- 思路:设置快慢指针fast,slow,两个指针同时从头结点开始走,fast每次向后移动两位,slow每次向后移动一位,当fast走到链表尽头时,slow指向的位置即为中间位置。
- 问题:若链表元素为偶数个,fast每次向后移动两位,在最后一次循环前,fast->next存在,所以fast->next->next的访问时合法的;当链表元素为奇数个时,fast每次向后移动两位则刚好将两边遍历完,fast->next不存在,所以访问fast->next->next是非法的
- 代码实现:
pNode FindMidNode(pList plist)
{
pNode fast = plist;
pNode slow = plist;
while (fast != NULL)
{
//fast->_next若为空,fast->_next->_next是非法的
if (fast->_next)
{
fast = fast->_next->_next;
slow = slow->_next;
}
else
{
fast = fast->_next;
}
}
return slow;
}
8.查找单链表的倒数第k个节点,要求只能遍历一次链表
pNode FindKNode(pList plist, int k);
- 思路:倒数第k个节点,即为第n-(k-1)个节点,我们要查找倒数第k个节点,只需要向前走n-(k-1)步,但是为了得到n的值我们还得遍历一次链表,总共需要遍历两次。借鉴上一题中的思路,我们可以考虑使用快慢指针来解决这个问题。让fast指针先走k-1步,然后fast,slow指针同时向后走,fast走到尽头时,slow指向的位置就是倒数第k个节点的位置
- 代码实现:
pNode FindKNode(pList plist, int k)
{
pNode fast = plist;
pNode slow = plist;
while (k - 1)
{
fast = fast->_next;
k--;
}//k-1==0,fast走到了第k-1个位置
while (fast->_next)
{
fast = fast->_next;
slow = slow->_next;
}
return slow;
}
9.判断链表是否带环
pNode CheckCircle(pList plist);
- 思路:定义两个指针fast,slow,两个指针同时从头开始走,fast指针每次走两步,slow指针每次走一步,如果fast和slow走的过程中相遇了(为了代码实现方便我们可以让fast先走一步,这样不会再次判断初始时候他们两个相等的情况了),则说明这个链表是带环的
- 代码实现:
pNode CheckCircle(pList plist)
{
if (plist == NULL || (plist)->_next == NULL)
{
return NULL;
}
//若带环,返回相遇节点;否则返回空
pNode fast = plist->_next;
pNode slow = plist;
while (fast != slow)
{
if (fast == NULL || fast->_next == NULL)
return NULL;
fast = fast->_next->_next;
slow = slow->_next;
}//fast==slow
return slow;
}
10.求环的长度
int GetCircleLength(pNode meet);
- 思路:我们首先通过图来分析一下快慢指针走的情况:
- 按照我们在判断链表是否带环题目中的想法,fast和slow同时走,fast每次走两步,slow每次走一步,若带环,则fast和slow回相遇。假设他们第一次相遇在z点,则此时fast走过的长度为:a+b+c+b;slow走过的长度为:a+b;因为fast速度是slow的两倍,所以有a+b+c+b=2(a+b);即a=c。而环的长度为b+c=b+a,所以从开始到二者第一次相遇,slow走过的长度即循环的次数即为环的长度。
- 若已知快慢指针相遇节点,即已知位置已经在环中了,则只需通过循环计数直到临时指针到达相遇位置为止,此时的count计的数即为环的长度
- 代码实现:
int GetCircleLength(pNode meet)
{
pNode tmp = meet;
if (meet == NULL)
return 0;
int count = 0;
while (tmp->_next != meet)
{
++count;
tmp = tmp->_next;
}//tmp->_next==meet;
return count;
}
11.求环的入口点
pNode GetCycleEntryNode(pList plist, pNode meet);
- 思路:已知相遇点和头结点,求环的入口节点。上述对第9题的分析中我们得到a=c,即头指针和相遇节点指针到环入口的距离是相等的。所以可以设置两个指针分别指向头结点和相遇节点,两个指针同时向后走,相遇的地方即为环的入口点
- 代码实现:
pNode GetCycleEntryNode(pList plist, pNode meet)
{
pNode p1 = plist;
pNode p2 = meet;
while (p1 != p2)
{
p1 = p1->_next;
p2 = p2->_next;
}//p1==p2
return p1;
}