数据结构整理(一) —— 链表的各种操作
马上要面临大规模的面试了,用了太久标准库,已经对数据结构的内部实现快忘了,趁着还有几天时间,自己又回忆了一下,用C++实现出来。所以接下来我在博客中会写一个“数据结构整理”系列,在面试之前,能回忆多少算多少吧,希望面试官能感受到我曾经是一个对数据结构很熟悉的人。。。
下面进入正题。
链表和数组都是线性表,数组就不多说了,说一下链表的实现。链表的名字很形象,就是一个一个的结点链在一起,必须找到一个结点的父节点,才能找到该结点,链表一般都有一个头结点,而且头结点一般不保存用户的数据,只是为了方便对链表的寻址。另外,可以在头结点中保存一些额外的信息,例如链表的长度,不过为了可读性,我个人不建议这样做。
下面是链表中一个结点的一般结构:
template <typename T> class Node { public: T data; Node *next; Node(T d) { data = d; next = nullptr; } Node() { next = nullptr; } };
data字段保存结点中需要存储的信息,next指针保存下一个结点的地址。
一般在链表中都有一个头指针和一个尾指针,头指针始终指向头结点,作用是方便对链表寻址。尾指针指向链表最后一个结点,作用是减少插入数据时消耗的时间。对链表的操作一般有获取长度,插入结点,删除结点,排序,逆置等,所以链表的一般结构如下:
1 template <typename T> 2 class LinkList { 3 private: 4 Node<T>* head; 5 Node<T>* rear; 6 int lenth; 7 public: 8 LinkList(); 9 ~LinkList(); 10 inline int size(); 11 void insert(T data); 12 void remove(T data); 13 void sort(); 14 void reverse(); 15 void getArray(T*& arr); 16 };
一个链表初始只有一个头结点,头指针和尾指针都指向头结点,链表长度为0。另外由于每个结点都是动态申请的堆内存,在析构函数中应及时释放,所以下面是构造函数和析构函数的实现:
1 template <typename T> 2 LinkList<T>::LinkList() { 3 head = new Node<T>(); 4 head->next = nullptr; 5 rear = head; 6 lenth = 0; 7 } 8 9 template <typename T> 10 LinkList<T>::~LinkList() { 11 Node<T>* cur = head->next; 12 while(cur != nullptr) { 13 delete head; 14 head = cur; 15 cur = cur->next; 16 } 17 }
向链表中插入数据即把新结点添加到链表末尾,所以利用尾指针可以很方便的做到。删除数据时先将待删除结点的父节点next的指针指向待删除结点的子节点,然后释放掉待删除结点的内存。下面是链表结点的插入和删除操作的实现:
1 template <typename T> 2 void LinkList<T>::insert(T data) { 3 Node<T>* cur = new Node<T>(data); 4 rear->next = cur; 5 rear = cur; 6 lenth++; 7 } 8 9 template <typename T> 10 void LinkList<T>::remove(T data) { 11 Node<T>* before = head; 12 Node<T>* cur = head->next; 13 while(cur != nullptr) { 14 if(cur->data == data) { 15 before->next = cur->next; 16 delete cur; 17 cur = before->next; 18 lenth--; 19 } else { 20 before = cur; 21 cur = cur->next; 22 } 23 } 24 }
链表的排序复杂度很高,一般不建议对存在大量数据的链表执行排序操作,链表的排序是一个对链表重建的过程,每次拆下一个结点,按照一定的顺序重新组合回去。注意排序完成后,尾指针一定要指向链表的最后一个结点。下面是链表排序的实现:
1 template <typename T> 2 void LinkList<T>::sort() { 3 Node<T>* p = head->next; 4 head->next = nullptr; 5 rear = head; 6 Node<T>* before = head; 7 Node<T>* after = head->next; 8 9 while(p != nullptr) { 10 if(after == nullptr || 11 (p->data >= before->data && p->data <= after->data)) { 12 before->next = p; 13 p = p->next; 14 before = before->next; 15 before->next = after; 16 before = head; 17 after = head->next; 18 if(rear->next != nullptr) { 19 rear = rear->next; 20 } 21 } else { 22 before = before->next; 23 after = after ->next; 24 } 25 } 26 }
链表的逆置也是一个重建链表的过程,即改变每个结点的指向,但是头结点依然是头结点。显然,逆置操作之后,尾指针应指向逆置前的第一个数据结点(即头指针的子结点)。下面是逆置操作的实现:
1 template <typename T> 2 void LinkList<T>::reverse() { 3 if(lenth < 2) { 4 return; 5 } 6 rear = head->next; 7 Node<T>* before = head->next; 8 Node<T>* after = before->next; 9 head->next = nullptr; 10 while(before != nullptr) { 11 before->next = head->next; 12 head->next = before; 13 before = after; 14 if(after != nullptr) { 15 after = after->next; 16 } 17 } 18 }
还有头文件中的另外两个函数,获取长度和转化为数组。获取长度很简单,在插入和删除时维护长度字段即可,为了提高在for循环中使用size()函数的效率,声明成inline的。转化为数组即遍历链表,把数据依次插入数组中即可。注意,C++不应返回一个函数中局部变量的数组,因为函数返回后栈被释放,非常危险。下面是获取长度和转化为数组的实现:
1 template <typename T> 2 int LinkList<T>::size() { 3 return lenth; 4 } 5 6 template <typename T> 7 void LinkList<T>::getArray(T*& arr) { 8 arr = new int[lenth]; 9 Node<T>* cur = head->next; 10 for(int i = 0; cur != nullptr; i++) { 11 arr[i] = cur->data; 12 cur = cur->next; 13 } 14 }
最后附上单元测试的代码:
1 void test_linklist() { 2 int number[] = {4, 2, 6, 7, 1, 4, 7}; 3 LinkList<int>* list = new LinkList<int>(); 4 for(int i = 0; i < 7; i++) { 5 list->insert(number[i]); 6 } 7 8 int* arr = nullptr; 9 list->getArray(arr); 10 cout << "init:" << endl; 11 for(int i = 0; i < list->size(); i++) { 12 cout << arr[i] << endl; 13 } 14 delete arr; 15 16 list->sort(); 17 list->getArray(arr); 18 cout << "after sort:" << endl; 19 for(int i = 0; i < list->size(); i++) { 20 cout << arr[i] << endl; 21 } 22 delete arr; 23 24 list->reverse(); 25 list->getArray(arr); 26 cout << "after reverse:" << endl; 27 for(int i = 0; i < list->size(); i++) { 28 cout << arr[i] << endl; 29 } 30 delete arr; 31 32 list->remove(4); 33 list->getArray(arr); 34 cout << "after remove 4:" << endl; 35 for(int i = 0; i < list->size(); i++) { 36 cout << arr[i] << endl; 37 } 38 delete arr; 39 40 delete list; 41 }