随机访问
与数组不同,链表如果想要随机访问就不同数组一样了。我们再访问某一个节点的时候,需要从头节点开始一个一个的往下寻找。因此时间复杂度是O(n)。
删除,插入操作
对于链表来说,删除以及插入的时间复杂度是O(1)。但是,我们要删除或者插入的时候,需要找到这个位置,寻找这个位置的过程的时间复杂度就不是O(1)了。
在实际的软件开发中,从链表中删除一个数据无外乎这两种情况:
- 删除结点中“值等于某个给定值”的结点。
- 删除给定指针指向的结点。
对于前一种情况来说,无论是单链表还是双向链表都需要从头开始,直到找到给定的值,因此时间复杂度都是O(n)。
而第二种情况来说,我们已经找到了需要删除的结点,但是我们需要找到该结点的前驱结点,如果我们使用的是单链表的话,我们还要重新遍历一次,因此时间复杂度是O(n),双向链表已经记录了前驱结点,所以可以在O(1)内完成。
同理如果我们希望在指定的结点前插入一个数据,双向链表也是如此。
还有就是对于一个有序的链表,我们在查找的时候,使用双向链表可以在上一次的位置,通过数据的大小,决定往前还是往后,这样大概节省了一半的时间。
给出末位删除以及插入的代码实现:
typedef struct Node{
int data;
Node* next;
}Node;
class LinkedList{
private:
Node* head = new Node;// 头指针,这里使用了带头结点的链表,简化实现难度
public:
LinkedList(){
head->data = 65535;
head->next = NULL;
}
void insert(int elem){
Node* newNode = new Node;
newNode->data = elem;
newNode->next = NULL;
newNode->next = head->next;
head->next = newNode;
}
bool Delete(int elem){
Node* p = head->next;//遍历指针
Node* ppre = head;// 前驱指针
for(;p!=NULL;p=p->next){
if(p->data == elem){
ppre->next = ppre->next->next;
return true;
}
ppre = p;
}
return false;
}
void print(){
Node* p = head->next;
for(;p!=NULL;p = p->next){
cout << p->data << endl;
}
}
};
空间换时间的设计思想
在内存空间足够的时候,我们可以选择空间复杂度高,但是时间复杂度相对低的算法,相反,如果空间很紧,那么我们就要选择空间复杂度低,时间复杂度高的算法了。这里的链表也是一样,对于数组来说,链表在存储数据的时候,往往要更加消耗空间,因为需要额外的指针变量,如果对于内存比较多的机器来说,我们可以选择双向链表,但对于内存比较吃紧的机器来说,我们优先使用数组。当然,这里的指针变量的消耗是相对来说的,如果说我们存储的数据远远大于指针变量的大小,那么指针变量的大小就可以忽略不计了。
利用哨兵简化实现难度
如果说,我们在结点p后面插入一个新的结点,只需要下面两行代码就可解决:
new_node->next = p->next;
p->next = new_node;
但是,如果我们要向一个空链表中插入第一个结点,刚刚的逻辑就不能使用了。我们需要进行下面特殊的处理,其中head表示链表的头结点。所以是这样的:
if (head == null) {
head = new_node;
}
我们再来看单链表结点删除操作。如果删除结点p的后继结点,我们只需要一行代码就可以搞定:
p->next = p->next->next;
但是,如果我们要删除链表中最后一个结点(只剩一个结点),前面的删除代码就不ok了。我们要改写成这样子:
if (head->next == null) {
head = null;
}
如果,我们引入哨兵结点,在任何时候,不管链表是不是空,head指针都会一直指向这个哨兵结点。我们也把这种有哨兵结点的链表叫带头链表。相反,没有哨兵的链表就叫作不带头链表。
这样的话,在空链表插入一个结点和删除最后一个结点的逻辑就可以统一了。
如何实现单链表反转?
实现的方法有很多,我觉得递归是最直观的了。我么假设一个链表有A,B,C,D,E,五个数据。我们假设BCDE已经反转好了,只要把这个整体指向A,那么问题转化为反转BCDE。如果我们假设CDE已经反转好了,那么问题就转化为反转CDE,以此类推,最后的问题转化为反转E。
这里给出递归代码的实现(这里采用了头结点,所以比较复杂,如果不用头结点就比较简洁了):
void reverse(){
if(head->next == NULL || head->next->next == NULL){
return;
}
recur_reverse(head->next);
}
Node* recur_reverse(Node* p){
if(p->next == NULL){
head->next = p;
return p;
}// 递归的终止条件
Node* newNode = recur_reverse(p->next);
newNode->next = p;// 将当前的结点指向上一个
p->next = NULL;
return newNode->next;// 返回指向的那个结点。
}
(这里的head是之前定义的类的私有变量)
测试代码:
#include <linkedlist.h>
int main(){
Linkedlist list;
list.Insert(10);
list.Insert(9);
list.Insert(8);
list.print();
list.reverse();
list.print();
}
如何实现将两个有序的链表结合成一个链表?
跟数组一样,我们创建一个新的链表L3,如何通过两个指针分别指向L1和L2链表,开始遍历,如果哪一个小就插入到新的L3,在某一个链表已经遍历完之后,再将剩下的接上去即可。这里的时间复杂度应该是O(n),空间复杂度也是O(n)。
这里给出代码实现:
typedef struct Node{
int data;
Node* next;
}Node,*LinkedList;
// 有序链表合并
void merge(LinkedList L1,LinkedList L2,LinkedList L3){
while(L1->next != NULL && L2->next != NULL){
if(L1->next->data <= L2->next->data){
L3->next = L1->next;
L1 = L1->next;
}
else{
L3->next = L2->next;
L2 = L2->next;
}
L3 = L3->next;
}
if(L1->next != NULL){
L3->next = L1->next;
}else{
L3->next = L2->next;
}
}
如何实现查找单链表的中间结点
思路:我们使用两个指针指向链表头结点,然后一个为fast,一个为slow,每次fast向前两步,slow向前一步,直到fast到达终点,slow指向的就是我们想要的的中间结点了。这里的时间复杂度是O(n)。然而如果我们使用一个计数的变量,记录这个链表到达终点的次数,然后取这个变量的中点,再遍历到该中点,虽然说这种方法是可行的,但相对第一种来说,遍历的次数就多了。
这里是代码实现(这里的head是之前定义的类的私有变量):
Node* midNode(){
if(head->next == NULL){
return NULL;
}
Node* fast = head->next;
Node* slow = head->next;
while(fast != NULL && fast->next != NULL){
fast = fast->next->next;
slow = slow->next;
}
return slow;
}
如何基于链表实现LRU缓存淘汰算法
我们维护一个有序的单链表,越靠近尾部的结点是越早之前访问的。当有一个新的数据被访问时候,我们从头开始顺序遍历链表。
- 如果这个数据在之前已经缓存在链表中了,我们就把它删除,然后把它插入到头结点之后。
- 如果这个数据没有在缓存中,这时有两种情况:
- 如果缓存没有满,那么我们直接把它插入到链表的头部。
- 如果缓存已经满了,我们把最后一个结点删除,然后把它插入到头部。
我们可以看到,无论是什么情况,我们都需要遍历一遍链表,所以我们缓存访问的时间复杂度是O(n)。实际我们可以使用散列表来记录数据的位置,这样就可以将O(n)复杂度降到O(1)。
如何检测链表中环的存在?
跟找中间结点一样,还是使用fast和slow两个指针,fast向前两步,slow一步,如果存在环,slow到达终点的时候,fast也到达终点了,这里的时间复杂度也是O(n)。
这里给出代码的实现(这里的head是之前定义的类的私有变量):
bool Check_loop(){
if(head->next == NULL){
return false;
}
Node* fast = head->next;
Node* slow = head->next;
while(fast->next != NULL && fast!=NULL){
fast = fast->next->next;
slow = slow->next;
if(fast == slow) return true;
}
return false;
}
如何实现判断是否是回文字符串(链表存储)?
我们通过使用fast和slow两个指针,fast向前两步,slow一步,直到终点。然后把slow后面的结点进行反转,然后和head到slow之间的数据进行一一比对。这里由于要遍历链表,所以时间复杂度是O(n),空间复杂度是O(1)。
这里给出代码实现(这里的head是之前定义的类的私有变量)
Node* reverse_c1(Node* head,Node* New){
if(head->next->next == NULL){
New->next = head->next;
return New->next;
}
Node* new_tail = reverse_c1(head->next,New);
new_tail->next = head->next;
head->next->next = NULL;
}
bool check_huiwen(){
if(head == NULL || head->next == NULL){
return false;
}
Node* fast = head->next;
Node* slow = head->next;
while(fast != NULL && fast->next != NULL){
fast = fast->next->next;
slow = slow->next;
}
Node* New = new Node;
reverse_c1(slow,New);
slow = New;
for(Node* i=slow->next;i!=NULL;i = i->next,head = head->next){
if(i->data != head->next->data){
return false;
}
}
return true;
}
单链表删除倒数第K个结点
void Delete_K(int k){
Node* p = head->next;
for(;p!=NULL;p = p->next){
k--;
}
if(k>0){
return;
}else{
Node* nP = head->next;
Node* nPPre = head;
while(k<0){
nPPre = nP;
nP = nP->next;
k++;
}
nPPre->next = nPPre->next->next;
}
}