链表
基础知识
一、*与&符号
a 本质上代表一个存储单元。CPU通过该存储单元的地址访问该存储单元中的数据。a中可以存放数值(10)和地址(336647)
例如b=10,&b=336647
&:取地址
a=&b:表示a = b存储单元的地址(336647)
a:代表获得a中存储的地址(336647)对应的存储单元(b)中的数据。也就是访问a就等于访问b
int b = 10;
int *a;//定义一个整形指针
a = &b;//给指针赋值,使指针指向b的地址
printf("%d", a);//输出的是b的地址
printf("\n");//换行符
printf("%d", *a);//*的作用是解引用,取出指针指向地址的内容,获得b10
return 0;
二、遍历链表而不改变指针位置的办法
struct ListNode
{
int data; //数据域
ListNode *next; // 指针域
ListNode(int x):data(x), next(NULL){};
};
ListNode leftHead(0);
//这种方式可以不改变leftHead指向
ListNode *leftptr = &leftHead;
leftptr = leftptr->next;
//如果是传参的形式
ListNode* xx(ListNode *head){
ListNode *p = head;
p = p->next;
// 这种情况时会改变head的指向的,所以之后要变回来
p = head;
}
三、8道经典链表面试常考题目
- 例:链表逆序
- 例2:链表求交点
- 例3:链表求环的入口
- 例4:链表划分
- 例5:复杂链表的复制
- 例6:2个排序链表归并
例1-a:链表逆序(easy)
链表反转的特点,原先的头的pre=NULL, next!=NULL;反转后pre!=NULL,next=NULL
原先的尾的pre=!NULL, next=null;反转后pre=NULL,next!=NULL
pNode每次让pNode->next断开原先的指向(下一个),转向前一个pPre, 之后pPre到pNode的位置, pNode再通过pNext向后走
1.初始:pNext-Null, pNode-1, pPre=Null
通过*pNext = pNode->next, 让pNext移到第2个位置
pNext-2, pNode-1, pNode->next-Null, pPre=Null
通过pNode->next = pPrev, 使得第1个位置指向pPre
pNext-2, pNode-1, pNode->next-Null, pPre=Null
通过pPrev = pNode, 让pPrev移到第1个位置
pNext-2, pNode-1, pNode->next-1, pPre-1
通过pNode = pNext, 让pNode移到第2个位置
pNext-2, pNode-2, pNode->next-1, pPre-1
2.第二次遍历:pNext-2, pNode-2, pPre-1
通过*pNext = pNode->next, 让pNext移到第3个位置
pNext-3, pNode-2, pNode->next-1, pPre-1
通过pNode->next = pPrev, 使得第2个位置指向pPre
pNext-3, pNode-2, pNode->next-1, pPre=1
通过pPrev = pNode, 让pPrev移到第2个位置
pNext-3, pNode-2, pNode->next-2, pPre-2
通过pNode = pNext, 让pNode移到第2个位置
pNext-3, pNode-3, pNode->next-2, pPre-2
struct ListNode
{
int data; //数据域
ListNode *next; // 指针域
ListNode(int x):data(x), next(NULL){};
};
class Solution
{
public:
ListNode* ReverseList(ListNode *pHead)
{
ListNode *pReversedHead = NULL;
ListNode *pNode = pHead;
ListNode *pPrev = NULL;
while (pNode != NULL)
{
ListNode *pNext = pNode->next; // pNode指向头结点,后续遍历列表每个节点
if(pNext == NULL){ //pNode到了最后一个结点,这时pNext指向NULL
pReversedHead = pNode;
}
pNode->next = pPrev; // 断开原先的指向(下一个),转向前一个pPre
pPrev = pNode; // pPre到pNode的位置
pNode = pNext; // pNode再通过pNext向后走
}
return pReversedHead;
}
};
例2:链表求交点
思路1:使用set存放遍历的链表1,在遍历列表2时判断set中是否已存在该结点
时间复杂度O(nlogn),空间复杂度O(n)
ListNode *getIntersectionNode(ListNode *pHead1, ListNode *pHead2){
std::set<ListNode*> node_set;
while (pHead1)
{
node_set.insert(pHead1);
pHead1 = pHead1->next;
}
while (pHead2)
{
if(node_set.find(pHead2) != node_set.end()){ // 未找到则返回node_set.end()
return pHead2;
}
pHead2 = pHead2->next;
}
return NULL;
}
思路2:遍历两链表长度,长的移动至和段的同一起点,两指针遍历一定有相交点。
时间复杂度O(n),空间复杂度O(1)
int getLen(ListNode *pHead){
int len=0;
while (pHead){
len++;
pHead = pHead->next;
}
return len;
}
ListNode *getIntersectionNode2(ListNode *pHead1, ListNode *pHead2){
int len1 = 0, len2 = 0;
len1 = getLen(pHead1);
len2 = getLen(pHead2);
if(len1 > len2){
for(int i = 0; i < len1-len2; i++)
pHead1 = pHead1->next;
while (pHead1 && pHead2){
if(pHead1 == pHead2)
return pHead1;
pHead1 = pHead1->next;
pHead2 = pHead2->next;
}
}
else{
for(int i = 0; i < len1-len2; i++)
pHead2 = pHead2->next;
while (pHead1 && pHead2){
if(pHead1 == pHead2)
return pHead1;
pHead1 = pHead1->next;
pHead2 = pHead2->next;
}
}
return NULL;
}
例3:链表求环的入口
思路1:遍历环,将遍历到的结点加入set中,每次检查set中是否有该结点, 有则说明该结点为入环结点。
ListNode* detectCycle(ListNode *pHead)
{
std::set<ListNode *> node_set;
while (pHead)
{
if(node_set.find(pHead)!=node_set.end()){
return pHead;
}
node_set.insert(pHead);
pHead = pHead->next;
}
return NULL;
}
思路2:快慢指针赛跑思想。快指针走2步,慢指针走1步,相遇说明有环;根据两个指针路程关系,可得起点。
方程解的结果为a=c,及在结点3处相遇。
// 思路2:快慢指针相遇
ListNode* detectCycle2(ListNode *pHead){
ListNode *fast = pHead;
ListNode *slow = pHead;
ListNode *meet = NULL;
while (fast) {
slow = slow->next;
fast = fast->next;
if(!fast) // 遇到链表尾为NULL,则无环return
return NULL;
fast = fast->next; // 多走1步
if(fast == slow){
meet = fast;
break;
}
}
if(meet == NULL) // 没有相遇则无环,meet为初值null
return NULL;
while (pHead && meet)
{
if(pHead == meet)
return pHead;
pHead = pHead->next;
meet = meet->next;
}
return NULL;
}
例4:链表划分
已知x值和链表头,将链表中小于x的放置在x前,大于的放后面,保持相对位置。
思路:使用两个临时指针空结点,遍历链表,将小于x的加入lefehead,大于x的加入righthead。
注意的是划分完之后,lefthead要链接到righthead的next结点上,righthead->next要置空。
注意.next和->next的区别
ListNode* partition(ListNode *head, int x){
ListNode leftHead(0);
ListNode rightHead(0);
ListNode *leftptr = &leftHead;
ListNode *rightptr = &rightHead;
while (head)
{
if(head->data < x){
leftptr->next = head;
leftptr = head;
}
else{
rightptr->next = head;
rightptr = head;
}
head = head->next;
}
leftptr->next = rightHead.next;
rightptr->next = NULL;
return leftHead.next;
}
例5:复杂链表的复制
在复杂链表中,每个节点除了有一个 next指针指向下一个节点,还有一个 random指针指向链表中的任意节点或者null。
复制复杂链表,即需要将原链表所有的链接关系都复制。
思路1:使用map和数组。map用于将原链表中元素映射到位置,如1,2,3,4,vector用于存入复制的新结点。再次遍历原链表,如果当前元素的random指向某一结点,则通过map获得指向结点的位置。通过vector获得数组中该位置对应的结点,将新结点random指向该结点。
ComplexListNode* copyRandomList(ComplexListNode *head){
std::map<ComplexListNode *, int> node_map;
std::vector<ComplexListNode *> node_vec; // 理解为新链表
ComplexListNode *ptr = head;
int i = 0;
while (ptr)
{
node_vec.push_back(new ComplexListNode(ptr->data)); // 复制结点, 存入数组
node_map[ptr] = i; // 为原链表中结点做map, 映射到元素顺序
ptr = ptr->next;
i++;
}
node_vec.push_back(0); // 最后一个结点指向这个0元素
ptr = head;
i = 0;
while (ptr)
{
node_vec[i]->next = node_vec[i+1]; // 将数组中元素链接
if(ptr->random){ // 如果原链表元素有random指向
// 获得该元素->random指向的结点的位置
int id = node_map[ptr->random];
// 将新的复制结点->random,指向新链表中该位置对应的结点
node_vec[i]->random = node_vec[id]; //数组的索引是1,2,3,4正好对应位置,所以可以通过位置索引得到该元素
}
ptr = ptr->next;
i++;
}
return node_vec[0]; // 返回新链表头部
}
思路2:
第一步根据原始链表的每个节点N创建对应的N’。把N’链接在N的后面。图中的链表经过这一步之后的结构如图所示。
第二步设置复制出来的节点的random。假设原始链表上的N的random指向节点S,那么其对应复制出来的N’是N的 next指向的节点,同样S’也是S的 random指向的节点。设置 random之后的链表如图所示。
第三步把这个长链表拆分成两个链表:把奇数位置的节点用 next链接起来就是原始链表,把偶数位置的节点用 next链接起来就是复制出来的链表。图中的链表拆分之后的两个链表如图所示。
struct ComplexListNode
{
int data; //数据域
ComplexListNode *next, *random; // 指针域
};
void CloneNodes(ComplexListNode *pHead){
// 第一步:复制结点
ComplexListNode *pNode = pHead;
while (pNode!=NULL)
{
ComplexListNode *pCloned = new ComplexListNode();
pCloned->data = pNode->data;
// 添加新节点,链接到原节点之后
pCloned->next = pNode->next;
pNode->next = pCloned;
pNode = pCloned->next;
pCloned->random = NULL;
}
}
void ConnectRandomNodes(ComplexListNode *pHead){
// 第二步:复制random指向
ComplexListNode *pNode = pHead;
while (pNode != NULL)
{
// pNode是第一个结点,pNode->next是复制出来的结点,每次都创建pCloned去指向该结点
ComplexListNode *pCloned = pNode->next;
if(pNode != NULL){
// pNode->random->next是原先结点random指向的结点的复制结点
pCloned->random = pNode->random->next;
}
pNode = pCloned->next;
}
}
ComplexListNode* ReconnectNodes(ComplexListNode *pHead){
// 第三步:将克隆结点从原链表中删除
ComplexListNode *pNode = pHead;
ComplexListNode *pClonedHead = NULL;
ComplexListNode *pClonedNode = NULL;
if(pNode != NULL){
pClonedHead = pClonedNode = pNode->next;
// 将pClonedNode从原链表中剔除,但pClonedHead指向pClonedNode
pNode->next = pClonedNode->next;
pNode = pNode->next;
}
while (pNode != NULL)
{
// 移动pClonedNode,指向下一个pClonedNode
pClonedNode->next = pNode->next;
pClonedNode = pClonedNode->next;
// 将pClonedNode从原链表中剔除
pNode->next = pClonedNode->next;
pNode = pNode->next;
}
return pClonedHead;
}
例6:2个排序链表归并
链表的归并不难理解,比较两个链表指针指向的数值,进行比对大小,再使用一个新指针链接到较小的那个值,原指针和新指针向后移动;当其中一个链表遍历完成时,跳出循环,未完成的链表将剩余的元素链接到新链表中。
class Solution
{
public:
// 递归方法存在问题,会缺少最后一个结点
ListNode* MergeList(ListNode *pHead1, ListNode *pHead2)
{
if(pHead1 == NULL){
return pHead1;
}
else if (pHead2 == NULL){
return pHead2;
}
ListNode *pMergedHead = NULL;
if(pHead1->data < pHead2->data){
pMergedHead = pHead1;
pMergedHead->next = MergeList(pHead1->next, pHead2);
}
else{
pMergedHead = pHead2;
pMergedHead->next = MergeList(pHead1, pHead2->next);
}
return pMergedHead;
}
ListNode* MergeList2(ListNode *pHead1, ListNode *pHead2)
{
ListNode tempHead(0);
ListNode *pre = &tempHead;
while (pHead1 && pHead2)
{
if(pHead1->data < pHead2->data){
pre->next = pHead1;
pHead1 = pHead1->next;
}else
{
pre->next = pHead2;
pHead2 = pHead2->next;
}
pre=pre->next;
}
if(pHead1){ //如果pHead1有剩余
pre->next = pHead1;
}
if(pHead2){
pre->next = pHead2;
}
return tempHead.next;
}
};