【编程篇】使用异常巧妙获取交叉链表的交点

这里的交叉链表,是Y型交叉链表。

话不多说,上代码:

首先定义一些用到的宏和链表节点,这里使用最简单的单向链表

1 #define ARRAY_SIZE(a)    sizeof((a)) / sizeof((a)[0])
2 #define ABS(a)            (a) > 0 ? (a) : (-(a))
3
4 typedef struct _Node
5 {
6     int data;
7     _Node* pNext;
8 }Node, *PNode;

下面是建立链表函数:

 1 /*
 2     新建链表
 3     若pTarget不为空,把新链表尾节点链到pTarget上
 4     若pTarget为空则新链表尾节点置空
 5 */
 6 PNode BuildList(PNode pTarget, int data[], int num)
 7 {
 8     PNode pHead = NULL;
 9     PNode pTail = NULL;
10     PNode p = NULL;
11
12     for (int i = 0; i < num; i++)
13     {
14         if (pHead == NULL)
15         {
16             pHead = new Node();
17             pHead->data = data[i];
18             pHead->pNext = NULL;
19
20             pTail = p = pHead;
21         }
22         else
23         {
24             p = new Node();
25             p->data = data[i];
26             p->pNext = NULL;
27
28             pTail->pNext = p;
29             pTail = p;
30         }
31     }
32
33     if (pTail && pTarget)
34     {
35         pTail->pNext = pTarget;
36     }
37
38     return pHead;
39 }

下面是遍历链表,打印所有节点数据的函数:

 1 /*
 2     遍历链表
 3 */
 4 void RecursiveList(PNode pHead)
 5 {
 6     PNode p = pHead;
 7     while (p)
 8     {
 9         printf("%d---", p->data);
10         p = p->pNext;
11     }
12 }

下面建立两个链表:

 1 int _tmain(int argc, _TCHAR* argv[])
 2 {
 3     int data1[] = {1, 2, 3, 4, 5, 6, 7, 8, 10086};
 4     int data2[] = {11, 22, 33, 44, 55, 66, 77, 88};
 5     PNode pHeadList1 = NULL;
 6     PNode pHeadList2 = NULL;
 7
 8     //
 9     // 建立链表1
10     //
11     pHeadList1 = BuildList(NULL, data1, ARRAY_SIZE(data1));
12     RecursiveList(pHeadList1);
13     printf("\n\n");
14
15     //
16     // 建立链表2
17     //
18     pHeadList2 = BuildList(NULL, data2, ARRAY_SIZE(data2));
19     RecursiveList(pHeadList2);
20     printf("\n\n");
21 }

输出结果如下:

下面,我们通过手动操作,让这两个链表构成Y型交叉链表:

 1 int _tmain(int argc, _TCHAR* argv[])
 2 {
 3     int data1[] = {1, 2, 3, 4, 5, 6, 7, 8, 10086};
 4     int data2[] = {11, 22, 33, 44, 55, 66, 77, 88};
 5     PNode pHeadList1 = NULL;
 6     PNode pHeadList2 = NULL;
 7     PNode pXXXNode = NULL;
 8
 9     //
10     // 建立链表1
11     //
12     pHeadList1 = BuildList(NULL, data1, ARRAY_SIZE(data1));
13     RecursiveList(pHeadList1);
14     printf("\n\n");
15
16     //
17     // 手动选取交叉节点
18     //
19     pXXXNode = pHeadList1->pNext->pNext->pNext;
20
21     //
22     // 建立链表2
23     //
24     pHeadList2 = BuildList(pXXXNode, data2, ARRAY_SIZE(data2));
25     RecursiveList(pHeadList2);
26     printf("\n\n");
27 }

这一次的输出结果如下:

可以发现,链表1与链表2在“4”这个节点相交了。

下面是比较常见的方法获取交点思路:

step 1: 使用两个指针指向两链表头,分别从头拨到尾,统计两个链表到终点的步数分别为 d1, d2。

step 2: 让两链表中距离交点远的一方指针先向后拨动 |d1-d2| 步,使两指针处于距离交点相同位置。

step 3: 然后两边指针同时向交点进发,每拨动依次就判断一次,看是否两指针指向了同一个点,若是,则到达交叉点

下面函数实现了上述算法:

 1 /*
 2     方法一:找到链表相交点
 3 */
 4 PNode FindIntersecNode(PNode ListLeft, PNode ListRight)
 5 {
 6     PNode pNode_X = NULL;
 7     PNode pTailLeft = NULL;
 8     PNode pTailRight = NULL;
 9     PNode p = NULL;
10     PNode q = NULL;
11
12     int nStepLeft = 0;
13     int nStepRight = 0;
14
15     //
16     // 遍历左边链表,统计步数,并定位尾节点
17     //
18     for (p = ListLeft; p; p = p->pNext)
19     {
20         nStepLeft++;
21         pTailLeft = p;
22     }
23
24     //
25     // 遍历右边链表,统计步数,并定位尾节点
26     //
27     for (p = ListRight; p; p = p->pNext)
28     {
29         nStepRight++;
30         pTailRight = p;
31     }
32
33     //
34     // 如果两链表尾节点不同,则两链表不相交
35     //
36     if (pTailLeft != pTailRight)
37     {
38         printf("These two list have no intersection.\n");
39         return NULL;
40     }
41     else
42     {
43         //
44         // 让步数多的一方先走 |nStepLeft - nStepRight| 步
45         //
46         p = nStepLeft > nStepRight ? ListLeft : ListRight;
47         q = nStepLeft < nStepRight ? ListLeft : ListRight;
48         int d = ABS(nStepLeft - nStepRight);
49         for (int i = 0; i < d; i++)
50         {
51             p = p->pNext;
52         }
53
54         //
55         // 接下来两边距离交点相同,一起向交点出发
56         //
57         while (p && q)
58         {
59             if (p == q)
60             {
61                 pNode_X = p;
62                 break;
63             }
64             p = p->pNext;
65             q = q->pNext;
66         }
67
68         return pNode_X;
69     }
70 }

通过调用此函数获得的交叉点可以和我们手动选取进行判断是否一致:

 1 int _tmain(int argc, _TCHAR* argv[])
 2 {
 3     int data1[] = {1, 2, 3, 4, 5, 6, 7, 8, 10086};
 4     int data2[] = {11, 22, 33, 44, 55, 66, 77, 88};
 5     PNode pHeadList1 = NULL;
 6     PNode pHeadList2 = NULL;
 7     PNode pXXXNode = NULL;
 8
 9     //
10     // 建立链表1
11     //
12     pHeadList1 = BuildList(NULL, data1, ARRAY_SIZE(data1));
13     RecursiveList(pHeadList1);
14     printf("\n\n");
15
16     //
17     // 选取交叉节点
18     //
19     pXXXNode = pHeadList1->pNext->pNext->pNext;
20
21     //
22     // 建立链表2
23     //
24     pHeadList2 = BuildList(pXXXNode, data2, ARRAY_SIZE(data2));
25     RecursiveList(pHeadList2);
26     printf("\n\n");
27
28     //
29     // 通过程序找到选取的交叉节点
30     //
31     PNode pXXXNode_Find = FindIntersecNode(pHeadList1, pHeadList2);
32
33     printf("选取的链表相交点:%d\n", pXXXNode->data);
34     printf("找到的链表相交点:%d\n", pXXXNode_Find->data);
35 }

执行结果如下:

可以发现,上面算法成功找出了交叉点,正是我们选取的那个点:“4”

下面,我们另辟蹊径,使用一个投机取巧的方式来找到这个点:

step 1: 遍历链表1,并把所有节点的pNext域加上0x80000000,使其指向系统内核地址空间。

step 2: 遍历链表2,使用__try __except捕获异常,当第一次出现访问异常,则当前指针就是交叉点的pNext域,如此可获取交叉节点

step 3: 重新遍历链表1,把所有pNext域减去0x80000000,恢复原有值。

下面是上面思路的实现:

 1 /*
 2     使用异常处理来获取交点
 3 */
 4 PNode FindIntersecNode_ByException(PNode ListLeft, PNode ListRight)
 5 {
 6     PNode pNode_X = NULL;
 7     PNode p = NULL;
 8
 9     //
10     // 遍历左边链表,将所有指针值加上0x80000000,使其指向内核区
11     //
12     p = ListLeft;
13     while (p)
14     {
15         PNode pTemp = p->pNext;
16         p->pNext = (PNode)((long)p->pNext + 0x80000000);
17         p = pTemp;
18     }
19
20     //
21     // 遍历右边链表,第一次出现访问异常则到了交叉点后一个节点
22     //
23     p = ListRight;
24     __try
25     {
26         PNode pTemp = NULL;
27         while (p)
28         {
29             pTemp = p;
30
31             //
32             // 这一句当p为内核区地址时将触发异常
33             // 后面pNode_X = pTemp;将不会得到执行
34             // 故pNode_X指向为上一轮的位置,也就是交点
35             p = p->pNext;
36
37             pNode_X = pTemp;
38         }
39     }
40     __except(1)
41     {
42         //
43         // 恢复左边链表的地址值
44         //
45         p = ListLeft;
46         while (p)
47         {
48             p->pNext = (PNode)((long)p->pNext - 0x80000000);
49             p = p->pNext;
50         }
51     }
52
53     return pNode_X;
54 }

下面使调用代码:

 1 int _tmain(int argc, _TCHAR* argv[])
 2 {
 3     int data1[] = {1, 2, 3, 4, 5, 6, 7, 8, 10086};
 4     int data2[] = {11, 22, 33, 44, 55, 66, 77, 88};
 5     PNode pHeadList1 = NULL;
 6     PNode pHeadList2 = NULL;
 7     PNode pXXXNode = NULL;
 8
 9     //
10     // 建立链表1
11     //
12     pHeadList1 = BuildList(NULL, data1, ARRAY_SIZE(data1));
13     RecursiveList(pHeadList1);
14     printf("\n\n");
15
16     //
17     // 选取交叉节点
18     //
19     pXXXNode = pHeadList1->pNext->pNext->pNext;
20
21     //
22     // 建立链表2
23     //
24     pHeadList2 = BuildList(pXXXNode, data2, ARRAY_SIZE(data2));
25     RecursiveList(pHeadList2);
26     printf("\n\n");
27
28     //
29     // 通过程序找到选取的交叉节点
30     //
31     PNode pXXXNode_Find = FindIntersecNode(pHeadList1, pHeadList2);
32     PNode pXXXNode_Find_ByException = FindIntersecNode_ByException(pHeadList1, pHeadList2);
33
34     printf("选取的链表相交点:%d\n", pXXXNode->data);
35     printf("找到的链表相交点:%d\n", pXXXNode_Find->data);
36     printf("[异常]找到的链表相交点:%d\n", pXXXNode_Find_ByException->data);
37 }

下面是执行结果:

可以发现,我们成功的找到了这个交点。

对于第一种方法:最坏情况下交点在最后一个,设两链表长度分别为a和b,那么最坏情况下两者都将遍历两遍 2a+2b = 2(a+b)

对于第二种方法:同样最坏情况下交点在最后一个,那么链表1由于对pNext域操作和恢复需要遍历两次2a,另外一个链表b,则总共2a+b,比上一种方法优越。

下面是释放一个单链表所有节点:

 1 /*
 2     释放链表节点内存
 3 */
 4 void FreeList(PNode pHead)
 5 {
 6     PNode p = pHead;
 7     PNode pTemp = NULL;
 8
 9     //
10     // 使用pTemp临时保存下一个节点地址
11     // 然后把当前节点释放
12     //
13     while (p)
14     {
15         pTemp = p->pNext;
16         delete p;
17         p = pTemp;
18     }
19 }

由于是Y型链表,那么释放的时候要注意交叉部分不能出现二次释放,将引发错误。

可将交叉点的前趋的pNext域置空,这样,先释放Y型的左边一个分叉,然后再释放右边分叉和下面的交集,代码如下:

 1 int _tmain(int argc, _TCHAR* argv[])
 2 {
 3     int data1[] = {1, 2, 3, 4, 5, 6, 7, 8, 10086};
 4     int data2[] = {11, 22, 33, 44, 55, 66, 77, 88};
 5     PNode pHeadList1 = NULL;
 6     PNode pHeadList2 = NULL;
 7     PNode pXXXNode = NULL;
 8
 9     //
10     // 建立链表1
11     //
12     pHeadList1 = BuildList(NULL, data1, ARRAY_SIZE(data1));
13     RecursiveList(pHeadList1);
14     printf("\n\n");
15
16     //
17     // 选取交叉节点
18     //
19     pXXXNode = pHeadList1->pNext->pNext->pNext;
20
21     //
22     // 建立链表2,将之链接到交叉节点pXXXNode上
23     //
24     pHeadList2 = BuildList(pXXXNode, data2, ARRAY_SIZE(data2));
25     RecursiveList(pHeadList2);
26     printf("\n\n");
27
28
29     //
30     // 通过程序找到选取的交叉节点
31     //
32     PNode pXXXNode_Find = FindIntersecNode(pHeadList1, pHeadList2);
33     PNode pXXXNode_Find_ByException = FindIntersecNode_ByException(pHeadList1, pHeadList2);
34
35     printf("选取的链表相交点:%d\n", pXXXNode->data);
36     printf("找到的链表相交点:%d\n", pXXXNode_Find->data);
37     printf("[异常]找到的链表相交点:%d\n", pXXXNode_Find_ByException->data);
38
39
40
41     //
42     // 把交叉点前趋的pNext域置为空,防止交叉部分二次释放
43     //
44     PNode pPreXXXNode = GetPreNode(pHeadList1, pXXXNode);
45     pPreXXXNode->pNext = NULL;
46
47     //
48     // 释放链表1和2
49     //
50     FreeList(pHeadList1);
51     FreeList(pHeadList2);
52
53     pHeadList1 = pHeadList2 = NULL;
54
55     printf("\n");
56     return 0;
57 }

其中获GetPreNode取前趋节点也很简单:

 1 /*
 2     由于是单向链表,只能直接获取后继,
 3     获取单链表中一个节点的前趋需要从头遍历
 4 */
 5 PNode GetPreNode(PNode pHead, PNode pNode)
 6 {
 7     PNode p = pHead;
 8     while (p && p->pNext != pNode)
 9     {
10         p = p->pNext;
11     }
12
13     if (p && p->pNext == pNode)
14         return p;
15     else
16         return NULL;
17 }

到这里就结束了。

说明一下:

1、仅仅是想着以一种新思路来解决传统问题,并没有一定要说谁优谁劣。另外获取交叉链表交点还有很多其他方法,比如构造环等。

2、这里为了达到效果,省去了很多异常检查和链表检查的代码,对传入的链表默认就是一个单向链表,不存在其他复杂的结构。

2、这里使用+0x80000000的方式存在着平台的限制,比如在Windows x86平台上适用,因为它们进程都是4GB地址空间,同时0x80000000以上是内核地址空间。要换了其他系统或者64位,则这种方法就不见得好用了。玩玩而已,呵呵

时间: 2024-10-23 06:35:03

【编程篇】使用异常巧妙获取交叉链表的交点的相关文章

C# 网络编程之通过豆瓣API获取书籍信息(一)

这篇文章主要是讲述如何通过豆瓣API获取书籍的信息,起初看到这个内容我最初的想法是在"C# 网络编程之网页简单下载实现"中通过HttpWebResponse类下载源码,再通过正则表达式分析获取结点标签得到信息.但后来发现可以通过豆瓣API提供的编程接口实现. 该文章仅是基础性C#网络编程文章,尝试测试了下豆瓣API,并没什么高深的内容.但希望对大家有所帮助,仅供学习. (警告:文章仅供参考,提供一种想法,否则访问多次-10次被403 forbidden莫怪.建议认证使用豆瓣API) 一

黑马程序员——网络编程篇

------- android培训.java培训.期待与您交流! ---------- 概述   1.网络模型        (1).OSI参考模型        (2).TCP/IP参考模型   2.网络通讯要素         (1).IP地址        (2).端口号         (3).传输协议    3.过程        1,找到对方IP. 2,数据要发送到对方指定的应用程序上.为了标识这些应用程序,所以给这些网络应用程序都用数字进行标识. 为了方便称呼这个数据,叫做端口(逻

[推荐]ORACLE PL/SQL编程之五:异常错误处理(知已知彼、百战不殆)

原文:[推荐]ORACLE PL/SQL编程之五:异常错误处理(知已知彼.百战不殆) [推荐]ORACLE PL/SQL编程之五: 异常错误处理(知已知彼.百战不殆) 继上三篇:ORACLE PL/SQL编程之八:把触发器说透 ORACLE PL/SQL编程之六:把过程与函数说透(穷追猛打,把根儿都拔起!) [推荐]ORACLE PL/SQL编程之四:把游标说透(不怕做不到,只怕想不到) 得到了大家的强力支持与建议,万分感谢.接下来介绍下一篇:oracle pl/sql异常处理部分,还望大家一定

【转】[推荐]ORACLE PL/SQL编程之五:异常错误处理(知已知彼、百战不殆)

[推荐]ORACLE PL/SQL编程之五: 异常错误处理(知已知彼.百战不殆) 继上三篇:ORACLE PL/SQL编程之八:把触发器说透 ORACLE PL/SQL编程之六:把过程与函数说透(穷追猛打,把根儿都拔起!) [推荐]ORACLE PL/SQL编程之四:把游标说透(不怕做不到,只怕想不到) 得到了大家的强力支持与建议,万分感谢.接下来介绍下一篇:oracle pl/sql异常处理部分,还望大家一定要支持与推荐呀~! 本篇主要内容如下: 5.1 异常处理概念 5.1.1 预定义的异常

零基础学习hadoop到上手工作线路指导(编程篇)

问题导读:1.hadoop编程需要哪些基础?2.hadoop编程需要注意哪些问题?3.如何创建mapreduce程序及其包含几部分?4.如何远程连接eclipse,可能会遇到什么问题?5.如何编译hadoop源码? 阅读此篇文章,需要些基础下面两篇文章零基础学习hadoop到上手工作线路指导(初级篇) 零基础学习hadoop到上手工作线路指导(中级篇)如果看过的话,看这篇不成问题,此篇讲hadoop编程篇. hadoop编程,hadoop是一个Java框架,同时也是编程的一次革命,使得传统开发运

XMPP-05Socket编程之网络编程篇

要学习XMPP,就要先了解Socket编程,在学习Socket之前,还要先了解一下网络编程 一.网络编程基本概念 通过使用套接字来达到进程间通信目的的编程就是网络编程. 网络编程从大的方面说就是对信息的发送到接收,中间传输为物理线路的作用,编程人员可以不用考虑…… 网络编程最主要的工作就是在发送端把信息通过规定好的协议进行组装包,在接收端按照规定好的协议把包进行解析,从而提取出对应的信息,达到通信的目的!中间最主要的就是数据包的组装,数据包的过滤,数据包的捕获,数据包的分析,当然最后再做一些处理

【编程题目】从尾到头输出链表(链表)☆

58.从尾到头输出链表(链表).题目:输入一个链表的头结点,从尾到头反过来输出每个结点的值.链表结点定义如下:struct ListNode{int m_nKey;ListNode* m_pNext;}; 我的思路:用一个数组存起来已有的数字,再反过来输出.缺点是数组大小是确定的 链表长度不能超过数组的大小 /* 58.从尾到头输出链表(链表). 题目:输入一个链表的头结点,从尾到头反过来输出每个结点的值.链表结点定义如下: struct ListNode { int m_nKey; ListN

Cocos2d-x3.0游戏实例之《别救我》第六篇——从代码中获取UI控件

这篇的内容很简单,获取UI控件,然后使用它. 还记得我们在UI编辑器中给三个按钮分别命名了吧? 现在要用上了. 笨木头花心贡献,啥?花心?不呢,是用心~ 转载请注明,原文地址: http://www.benmutou.com/blog/archives/918 文章来源:笨木头与游戏开发 根据名字查找控件 首先给TollgateScene再include一些头文件,不然等会编译又报错了: #include "editor-support/cocostudio/CCSGUIReader.h&quo

《高性能javascript》 领悟随笔之-------DOM编程篇(二)

<高性能javascript> 领悟随笔之-------DOM编程篇二 序:在javaSctipt中,ECMASCRIPT规定了它的语法,BOM实现了页面与浏览器的交互,而DOM则承载着整个页面文档.DOM编程性能一直以来都是非常受开发者关注的话题,如何编写高性能的DOM是前端开发必不可少的技能. 1.重绘与重排 当浏览器加载完页面所有的元素.js.css.图片之后会自动生成两个数据结构: 1.dom树 (图片为转载) 如图所示,dom树表示了整个页面文档的结构,通过访问dom树我们可以得到某