链表
最近在学数据结构,其实之前学到过链表,不过只是看了看书,没写课后题也没做什么练习。昨天看面试题,发现大多数都是单链表,而我学的都是双链表0 0于是今天花了一天写了写单链表,以及单链表的经典题目:链表翻转,链表归并排序。还没学到快排,等学到了再补上...
首先总结一下链表的特性。常见的线性表有两种:向量和链表(栈和队列也可以算线性表,可以通过继承前两种基本结构并封装来获得)。
向量是一个在物理内存上连续的数据结构,STL中vector<type>的内部维护一个数组,根据动态操作相应地扩容或者缩容,高效利用内存。向量最大的优点在于可以循秩访问(call by position),但是要求必须内存物理连续。
链表是一个存在逻辑次序的数据结构,在物理内存上未必是连续的,而是在节点中存在指针指向下一个节点(单向链表)或者指向前一个和后一个节点(双向链表)(call by link)。链表也可以根据逻辑次序循秩访问。链表相对于向量最大的优势是可以充分利用内存而不要求物理内存是连续的,相应地,这也有一定的代价。
对于几种常用的操作,访问、查找、删除、添加,向量和链表存在一些差异,即使是单链表和双链表,也是不同的。
对于向量,访问的时间复杂度为O(1),线性查找为O(n),二分查找为O(logn),删除为O(n),添加为O(n)。
对于单链表,访问只能从头开始寻找,复杂度为O(n),链表难以进行二分查找,线性查找的复杂度也是O(n),按位置删除和添加因为需要先从头寻找位置,故为O(n),如果在尾部或者头部设置了哨兵节点,尾部或者头部的复杂度仅为O(n)。
双链表在许多方面比单链表性能有很大提高。双链表在任意位置可以在O(1)的时间内插入节点,删除已知的节点也为O(1)。
链表的基本操作可以参见各种教材,下面记录一下几个经典问题。
单链表的链表翻转
解题思路:随时记录三个指针,分别是当前节点,前驱和后继。修改当前节点的指向,然后移动前驱和后继。最后单独把剩余的节点指向更改,如果存在header,最后更改header,使其指向当前节点。
1 class listNode { 2 public: 3 int data; 4 listNode* next; 5 listNode(int e) :data(e),next(NULL) { } 6 };
1 listNode* reverse(listNode*& head) 2 { 3 if (head == NULL || head->next == NULL) return head; 4 listNode* pre = NULL; 5 listNode* curr = head; 6 listNode* succ = NULL; 7 while (curr->next != NULL) 8 { 9 succ = curr->next; 10 curr->next = pre; 11 pre = curr; 12 curr = succ; 13 } 14 curr->next = pre; 15 return curr; 16 }
实现还是很简单的,太懒了用电脑不想画图,用手画又太丑了...于是就没图了,思考的时候一定要画个图,事半功倍!
单链表的归并排序
归并排序的方法还是非常简单的,按照一般的二路归并,L1、L2两个头节点作为归并的起点,结果保存在L1中。需要注意的地方,一个是如何把L2的节点加入到L1中(可以画个图),另外一个就是选择递归的分割点。为了方便在L1前面插入,设置了一个header作为哨兵节点。用快慢指针的方法,找到中点(当然直接遍历完int m=n>>1也是没什么问题的)。
1 listNode* merge(listNode*& L1, listNode*& L2) 2 { 3 listNode header(-1), *p = &header, *q = L2; //L1的哨兵节点header,因为可能在头部插入 4 header.next = L1; 5 while (p->next != NULL && q != NULL) //停止条件,同时包括了判断两个链表是否为空 6 { 7 if (p->next->data >= q->data)//L2比L1的元素大 8 { 9 L2 = q->next; 10 q->next = p->next; 11 p->next = q; 12 p = q; 13 q = L2; 14 } 15 else 16 p = p->next; 17 } 18 if (p->next == NULL) //L2可能还有未处理的结点,直接加在L1尾部 19 p->next = q; 20 return header.next;//返回L1 21 } 22 listNode* mergeSort(listNode*& head) 23 { 24 if (head == NULL || head->next == NULL) //size<2直接返回 25 return head; 26 listNode *slow = head, *fast = head; 27 while (fast->next != NULL && fast->next->next != NULL) 28 { 29 fast = fast->next->next; 30 slow = slow->next; 31 } 32 listNode *leftHead = head, *rightHead = slow->next; 33 slow->next = NULL; //把前半部分的尾结点的next赋NULL 34 leftHead = mergeSort(leftHead); 35 rightHead = mergeSort(rightHead); 36 return merge(leftHead, rightHead); 37 }
双链表
双链表的反转以及归并排序,比单链表要简单一些。双链表的反转,可以前后遍历一遍改变前驱后继,也可以从两端交换节点的data值。需要注意的是,后面一种方法,在链表存储数据的类型是内置类型的时候,效率可能很高。如果是自定义的类类型,就要视情况而定。如果类十分复杂,这种方式效率可能很低。
双链表的归并排序,代码如下。List<typename T>是一个比较完善的类类型,详细可参考邓俊辉老师的《数据结构(C++语言版)》。
1 typedef int rank; 2 #define ListNodePosi(T) ListNode<T>* 3 template<typename T> class ListNode { 4 public: 5 T data; 6 ListNodePosi(T) succ; 7 ListNodePosi(T) pred; 8 9 ListNode()= default; 10 ListNode(T e, ListNodePosi(T) p = NULL, ListNodePosi(T) s = NULL) 11 :data(e), pred(p), succ(s) {} 12 13 ListNodePosi(T) InsertAsPred(T const& e); 14 ListNodePosi(T) InsertAsSucc(T const& e); 15 }; 16 17 void Merge(List<int>& L1, ListNodePosi(int)& p, int n, List<int>& L2, ListNodePosi(int)& q, int m)//归并到L1中 18 { 19 int i = 0, j = 0; 20 ListNodePosi(int) pp = p->pred; 21 while (j < m) 22 if (i < n && (p->data <= q->data)) 23 { 24 if (q == (p = p->succ)) break; i++; 25 } 26 else 27 { 28 L1.InsertAsP(p, L2.remove((q = q->succ)->pred));j++; 29 } 30 p = pp->succ; 31 } 32 void listMerge(List<int>& L, ListNodePosi(int)& p, int n) 33 { 34 if (n < 2) return; 35 int m = n >> 1; 36 ListNodePosi(int) q = p; for (int i = 0; i < m; i++) q = q->succ; 37 listMerge(L, p, m); listMerge(L, q, n - m); 38 Merge(L, p, m, L, q, n - m); 39 }
就先写这些了,后续再补充0 0第一次写博客文章,也不知道自己以后会不会看...希望是不会看了,因为都熟练了不会回头再看了^ ^