链表是一种非常重要的数据结构,在笔试和面试时经常会遇到,所以自己总结了一下。
本来两天前就该写好的,只是一直在想其他的事。最近两天感觉很累,这雪已经下了两天了,顶风冒雪的奔波,本想有个好结果,但好像又是一场空。今年为什么什么事都不顺,事业、感情,都是他妈的一塌糊涂,真想骂两句,可是就不知道骂谁,一切也只能骂自己!
1.单链表定义
回归正题。链表的定义,我摘自维基百科。重复看一下吧。“链表(Linked list)是一种常见的基础数据结构,是一种线性表,但是并不会按线性的顺序存储数据,而是在每一个节点里存到下一个节点的指针(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!目前只总结了这些问题,如果以后遇到新问题再更新吧!