剑指LeetCode面试算法:链表

链表

基础知识
一、*与&符号

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道经典链表面试常考题目
  1. 例:链表逆序
  2. 例2:链表求交点
  3. 例3:链表求环的入口
  4. 例4:链表划分
  5. 例5:复杂链表的复制
  6. 例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;
    }
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值