有关单链表的一些问题总结
2012-11-13 20:23
323 查看
链表是一种非常重要的数据结构,在笔试和面试时经常会遇到,所以自己总结了一下。
本来两天前就该写好的,只是一直在想其他的事。最近两天感觉很累,这雪已经下了两天了,顶风冒雪的奔波,本想有个好结果,但好像又是一场空。今年为什么什么事都不顺,事业、感情,都是他妈的一塌糊涂,真想骂两句,可是就不知道骂谁,一切也只能骂自己!
list)是一种常见的基础数据结构,是一种线性表,但是并不会按线性的顺序存储数据,而是在每一个节点里存到下一个节点的指针(Pointer)。由于不必须按顺序存储,链表在插入的时候可以达到O(1)的复杂度,比另一种线性表顺序表快得多,但是查找一个节点或者访问特定编号的节点则需要O(n)的时间,而顺序表相应的时间复杂度分别是O(logn)和O(1)。”链表与数组相比的优缺点:使用链表结构可以克服数组链表需要预先知道数据大小的缺点,链表结构可以充分利用计算机内存空间,实现灵活的内存动态管理。但是链表失去了数组随机读取的优点,同时链表由于增加了结点的指针域,空间开销比较大。
归并排序:
冒泡排序:
思路2:两个链表相交,那么只有Y字形状的。所以只要顺序链表两个链表到尾端,如果尾端相同,那么就是有交点,否则就没有交点。
Ok,That's all!目前只总结了这些问题,如果以后遇到新问题再更新吧!
本来两天前就该写好的,只是一直在想其他的事。最近两天感觉很累,这雪已经下了两天了,顶风冒雪的奔波,本想有个好结果,但好像又是一场空。今年为什么什么事都不顺,事业、感情,都是他妈的一塌糊涂,真想骂两句,可是就不知道骂谁,一切也只能骂自己!
1.单链表定义
回归正题。链表的定义,我摘自维基百科。重复看一下吧。“链表(Linkedlist)是一种常见的基础数据结构,是一种线性表,但是并不会按线性的顺序存储数据,而是在每一个节点里存到下一个节点的指针(Pointer)。由于不必须按顺序存储,链表在插入的时候可以达到O(1)的复杂度,比另一种线性表顺序表快得多,但是查找一个节点或者访问特定编号的节点则需要O(n)的时间,而顺序表相应的时间复杂度分别是O(logn)和O(1)。”链表与数组相比的优缺点:使用链表结构可以克服数组链表需要预先知道数据大小的缺点,链表结构可以充分利用计算机内存空间,实现灵活的内存动态管理。但是链表失去了数组随机读取的优点,同时链表由于增加了结点的指针域,空间开销比较大。
2.单链表相关问题
链表的结构定义如下代码,为了在后面测试使用,我写了一个利用数组来生产一个链表的函数。代码如下:typedef struct LinkedList { int data; LinkedList *next; }node; node *CreateList(int data[],int len) { node *head = new node; head->data = data[0]; head->next = NULL; node *p = head; for(int i = 1; i < len; ++i) { node *tmp = new node; tmp->data = data[i]; tmp->next = NULL; p->next = tmp; p = tmp; } return head; }
2.1 单链表的遍历
由于链表不像数组那么可以直接根据下标来遍历值。如果想要在链表中找到某个值,必须依次遍历链表,最坏时要遍历所有节点,所以单链表的遍历时间复杂度为O(n)。如果我们想遍历单链表,一个主要的依据就是单链表的尾节点的next是空,我们根据这个性质就可以依次遍历单链表了。代码如下:// 遍历链表 void print(node *head) { if(head == NULL) { cout << "List is empty!" << endl; return; } node *p = head; while(p) { cout << p->data << " "; p = p->next; } }
2.2 删除给定头结点单链表中指定的值
对于删除操作,在单链表中,影响最大就是单链表的指针域,毕竟,单链表只有靠指针域才能找到下一个节点在内存中的位置。在这里,我们没有考虑删除链表中重复的几个数。删除指定值的节点,首先要找到这个节点,同时要记录下此节点的前驱结点,因为我们要修改前驱节点的指针域,以保证链表的连续。找到此节点后,我们要考虑三种情况。第一种就是此节点是头结点。我们不能简单的删除头结点,因为对于这个链表我们只有头结点的信息。可以将头结点的后继节点复制给头结点,并删除后继节点,此时就相当于删除了原头结点。第二种情况就是此节点为尾节点。此时,比较简单,我们直接将前驱节点的指针域赋值为空,然后删除尾节点即可。第三种情况是为中间节点。此时,我们可以将此节点的指针域赋给前驱结点的指针域,然后删除此节点。具体代码如下:// 给出头结点,删除指定的值 void DeleteNode(node *head,int num) { if(head == NULL) { cout << "List is NULL."; return; } node *p = head; node *q ; while(p) { if(p->data != num) { q = p; // p的前驱节点 p = p->next; if(p == NULL) { cout << "未找到此节点!"; break; } } else break; } if(p == head) // p为头结点 { q = head->next; if(q == NULL) { head = NULL; delete head; } head->data = q->data; head->next = q->next; delete q; } else if(p->next == NULL) // p为尾节点 { q->next = NULL; delete p; } else // p为中间节点 { q->next = p->next; delete p; } }
2.3 在一个无头节点的链表中,给定一个随机的节点指针(不是第一个,也不是最后一个)删除它
此时的问题,不同于上面的删除,此时没有头结点,所以你找不到要删除节点的前驱节点。现在只有当前节点的一个指针。我们根据这个指针只能找到它的后继节点。因为无法修改前驱节点的指针域,我们就变通的去实现。现在有后继节点,可以将后继节点赋给此节点,那么此时这个节点就是它的后继节点,然后删除后继节点,这样就实现了删除随机节点。代码如下:void DeleteRandomNode(node *p) { if(p == NULL) return; node *q = p->next; if(q != NULL) { p->next = q->next; p->data = q->data; delete q; } }
2.4 顺序翻转单链表
翻转单链表,其实也就是将指针域倒过来。重点是,我们既要让原链表顺序向后走,又要修改指针域,所以我们需要记录一些指针域的值,以保证上面的操作。不多说了,直接上代码:// 顺序翻转链表 node* Reverser(node *head) { if(head == NULL) return NULL; node *p1 = head; node *p2 = head->next; while(p2) { node *p3 = p2->next; p2->next = p1; p1 = p2; p2 = p3; } head->next = NULL; return p1; }
2.5 在节点p后面插入一个节点
在节点p后面插入一个节点,很直接,即只要修改p和新增节点的指针域即可。直接上代码:// 在节点p后面插入一个节点 void InsertNodeAfterP(node *p,int num) { node *t = new node; t->data = num; if(p->next == NULL) { p->next = t; t->next = NULL; } else { t->next = p->next; p->next = t; } }
2.6 在节点p前面插入一个节点
在节点p前面插入一个节点,相对于上面的问题,需要多想一步。在p前面插入一个节点,因为无法得知前驱节点,所以不能直接修改指针域。所以,我们只能将新增节点插入到p后面,那么如何符合题意呢?既然能插入到p后面,那么现在只是顺序不一样,所以我们颠倒一下这两个节点不就符合题意了嘛。颠倒,也就是将值交换一下。代码如下:// 在节点p前面插入一个节点 void InsertNodeBeforeP(node *p,int num) { InsertNodeAfterP(p,num); node *q = p->next; q->data = p->data; p->data = num; }
2.7 链表的排序
链表的排序,比数组中排序复杂一点,主要是涉及到了指针域的操作,所以有些排序算法不实用与链表中,比如快速排序算法不适合链表的排序。链表的排序最好的应该就是归并排序,因为没有生产新的节点,辅助空间为O(1),时间复杂度为O(nlogn)。冒泡排序可以用到这里,这里会有不可避免的值的交换。以下代码中,冒泡排序是自己写的,归并排序是在别的地方抄下来的,并没有测试。归并排序:
node *linkedListMergeSort(node *pHead) { int len = getLen(pHead); return mergeSort(pHead,len); } node *mergeSort(node *p,int len) { if(len == 1) { p->next = NULL; return p; } node *pmid = p; for(int i = 0; i < len/2; i++) { pmid = pmid->next; } node *p1 = mergeSort(p,len/2); node *p2 = mergeSort(pmid,len-len/2); return merge(p1,p2); } node *merge(node *p1,node *p2) { node *p = NULL,*ph = NULL; while(p1 != NULL && p2 != NULL) { if(p1->data < p2->data) { if(ph == NULL) { ph = p = p1; } else { p->next = p1; p1 = p1->next; p = p->next; } } else { if(ph == NULL) { ph = p = p2; } else { p->next = p2; p2 = p2->next; p = p->next; } } } p->next = (p1 == NULL) ? p2 : p1; return ph; }
冒泡排序:
// 链表排序 void SortList(node *head) { if(head == NULL || head->next == NULL) return; node *p = head; int len = 0; while(p) { len++; p = p->next; } p = head; int num = 0;// 记录比较的次数 while(len) { while(p->next) { if(p->data > p->next->data) { int tmp = p->data; p->data = p->next->data; p->next->data = tmp; } p = p->next; num++; if(num == len) break; } p = head; num = 0; len--; } }
2.8 只遍历一次找到中间节点
对于此问题,如果去掉只遍历一次的限制,我们可以先遍历以下链表记录下链表的长度,然后折半,就可以找到中间的节点,但是此时已经遍历第二次了。我们可以设置两个指针,都从头结点开始遍历,一个节点每次走一步,另一个每一次走两步,那么当后一个节点到达末尾时,前面的那个节点岂不是正好到达中间!node* SearchMid(node *head) { node *p1 = head; node *p2 = head; while(1) { if(p1->next != NULL && p2->next->next != NULL) { p1 = p1->next; p2 = p2->next->next; } else break; } return p1; }
2.9 输入一个单向链表,输出该链表中倒数第k个节点。链表的倒数第0个节点为链表尾指针
此题为上面的一个普遍形式吧。思路一:首先遍历以下链表得到链表的长度,然后从头开始遍历len-k个节点,就是倒数第k个节点。思路二:设置两个指针p1和p2.p1先从头结点开始遍历链表,遍历k个节点。然后,p2从头节点开始遍历,p1从当前位置开始遍历,当p1到达末尾时,p2就到达了倒数第k个节点。node *LastK1(node *head,int K) { if(head == NULL) return NULL; node *p = head; int len = 0; while(p != NULL) { len++; p = p->next; } p = head; int num = len - K; while(p) { num--; if(num == 0) break; p = p->next; } return p; } node *LastK2(node *head,int K) { if(head == NULL) return NULL; node *p1 = head; node *p2 = head; while(K+1) { K--; p1 = p1->next; } while(p1 != NULL) { p1 = p1->next; p2 = p2->next; } return p2; }
2.10 判断一个单链表是否有环
思路:使用两个指针p1,p2.从头开始遍历,p1每次前进一步,p2每次前进两步。如果p2到达了链表尾部,说明无环,否则p1 p2必然会在某个时刻相遇,从而检测到链表中有环。// 判断是否有环 bool IsCycle(node *head) { if(head == NULL) return false; node *p1 = head; node *p2 = head; while(p1->next) { p1 = p1->next; p2 = p2->next->next; if(p1 == p2) { break; return true; } else continue; } if(p1->next == NULL) return false; else return true; }
2.11 给定两个单链表,检测两个链表是否有交点,如果有返回第一个交点
思路1:将两个链表首尾相连,合并成一个链表,如果有交点,那么就会有环。从而判断出是否有交点。要找第一个交点,可以首先得到两个链表的长度,len1,len2,假设len1>len2,那么p1先向前走len1-len2步,然后p1和p2同时前进,当p1==p2时,那就是第一个交点。思路2:两个链表相交,那么只有Y字形状的。所以只要顺序链表两个链表到尾端,如果尾端相同,那么就是有交点,否则就没有交点。
node *IsHaveCrossingNode1(node *head1,node *head2) { node *p1 = head1; node *p2 = head2; node *p3 = head2; while(p2->next) p2 = p2->next; p2->next = p1; // p1 p2合并成一个链表 bool mask = IsCycle(p3); p2->next = NULL; // 断开链表 p1 = head1; p2 = head2; int len1 = 0; int len2 = 0; while(p1) { len1++; p1 = p1->next; } while(p2) { len2++; p2 = p2->next; } p1 = head1; p2 = head2; if(mask) // 有交点时,求交点 { if(len1 > len2) { int n = len1 - len2; while(n-1) { p1 = p1->next; n--; } while(p1->next!= NULL && p2->next != NULL) { p1 = p1->next; p2 = p2->next; if(p1 == p2) { break; return p1; } } } } else { return NULL; } } bool IsHaveCrossingNode(node *head1,node *head2) { if(head1 == NULL || head2 == NULL) return false; node *p1 = head1; node *p2 = head2; while(p1->next) { p1 = p1->next; } while(p2->next) { p2 = p2->next; } if(p1 == p2) return true; else return false; }
Ok,That's all!目前只总结了这些问题,如果以后遇到新问题再更新吧!
相关文章推荐
- 有关壳问题的一些总结
- 有关vhdl的一些问题总结
- 有关网页抓取问题的一些经验总结
- 有关网页抓取问题的一些经验总结 - passover【毕成功的博客】 - 51CTO技术博客
- Tivoli实施中一些问题的总结(官方)
- 近期学习javascript和jquery遇到一些问题的技巧知识总结
- 我想告诉你的一些有关质量问题的答案
- 直播技术总结(三)ijkplayer的一些问题优化记录
- Web前端页面的浏览器兼容性测试心得(三)总结一些IE8兼容问题的解决方案
- 关于ASP.NET在IIS一些问题的经验总结
- android处理图片的一些问题总结
- 总结移动端页面开发时需要注意的一些问题
- 关于ASP.NET在IIS一些问题的经验总结
- 项目开发中经常遇到的一些问题总结
- Oracle Form Builder配置问题的一些总结
- 安装linux遇到的一些问题总结
- 【原创】越狱以后的iPhone从4.3.3升级到5.0.1时遇到的一些问题的解决办法总结
- findbug 发现的一些隐藏问题总结
- 大数据处理的一些总结和应用(有关舆情监控)
- 关于absolute定位的一些问题总结