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

这里的交叉链表,是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位,则这种方法就不见得好用了。玩玩而已,呵呵

posted @ 2014-09-19 11:15  轩辕之风  阅读(1047)  评论(1编辑  收藏  举报