【链表】常见算法题

struct ListNode  //定义链表
{
	int key;          //值
	ListNode * pnext;  //指针
}

1.求单链表中结点的个数

就是最简单的循环计数

int calculate(ListNode *pHead)
{
	if(pHead==NULL)
		return 0;
	ListNode *p = pHead;
	int nLength = 0;
	while(p!=NULL)
	{
		p=p->pNext;
		nLength++;
	} 
	return nLength;
}

2.将单链表反转

采用“头插法”,顺序遍历原链表,挨个将结点头插到新链表中

(注意要先把这个结点给copy一下,去把副本进行头插,不然的话原链表关系就断了)

这样的头插法可以完美反转链表顺序。

注意最开始要考虑链表为空OR链表只有一个结点(反转无用)。

时间复杂度为O(n)

//头插法!!!
ListNode * reverse(ListNode * pHead)
{
	if(pHead==NULL || pHead->pNext==NULL) //该链表为空OR只有一个结点
		return NULL;
	ListNode* reverseHead=NULL; //结果链表的表头,注意要初始化为NULL! 
	ListNode* p = pHead;
	while(p!=NULL)
	{
		ListNode* p_temp = p; //当前要移动的结点 
		p = p->pNext; //循环要用的对于原链表的指针赶紧向后移,指向下一个要移动的结点
		p_temp->pNext = reverseHead;
		reverseHead = p_temp; //头插法 
	}
	return reverseHead; 
} 

 

3. 查找单链表中的倒数第k个结点(k>0)

最普遍的方法是,先统计单链表中结点的个数,然后再找到第(n-k)个结点。注意链表为空,k为0,k为1,k大于链表中节点个数时的情况。时间复杂度为O(n)。代码略。

这里主要讲一下另一个思路,这种思路在其他题目中也会有应用。

主要思路就是使用两个指针,先让前面的指针走到正向第k个结点,这样前后两个指针的距离差是k-1,之后前后两个指针一起向前走,前面的指针走到最后一个结点时,后面指针所指结点就是倒数第k个结点。

要注意特殊情况(k==0 链表为空 链表还没k长)

ListNode * lastK(ListNode * pHead , int k)
{
	if(pHead==NULL||k==0) //该链表为空 OR k==0
		return NULL;
	ListNode* after=pHead;
	ListNode* before=pHead;
	
//	for(int i=0;i<k-1;i++)  //让p走到第k个结点 
//		before=before->pNext; 
	while(k>1 && before!=NULL)
	{
		before=before->pNext;
		k--;
	}
	if(k>1) //说明链表长度<k
		return NULL; 
	
	while(before->pNext!=NULL) 
	{
		before=before->pNext;
		after=after->pNext;
	} 
	//退出while,说明before到达最后一个结点
	//after一直和before相差k-1
	return after; 
} 

 

4. 查找单链表的中间节点

此题可应用于上一题类似的思想。也是设置两个指针,只不过这里是,两个指针同时向前走,前面的指针每次走两步(那么前面的指针总是比他多走一倍),后面的指针每次走一步,前面的指针走到最后一个结点时,后面的指针所指结点就是中间结点,即第(n/2+1)个结点。

注意链表为空,链表结点个数为1和2的情况——也就是在遍历中有可能让指针指向空的情况

时间复杂度O(n)

ListNode * lastK(ListNode * pHead)
{
	if(pHead==NULL) //该链表为空
		return NULL;
	ListNode* after=pHead;
	ListNode* before=pHead;
	
	while(before->pNext!=NULL && before->pNext->pNext!=NULL) //注意条件语句判断
	{

	    before = before->pNext->pNext; 
	    after = after->pNext; //后面的指针一次走一步 
	}
	
	return after; 
} 

 

5. 从尾到头打印链表

当然,用栈or递归!

stack<ListNode *> s;
void rPrintList(ListNode* pHead)
{
	ListNode *p=pHead;
	while(p!=NULL)
	{
		s.push(p);
		p=p->pNext;
	}
	//全部放入栈中后,依次出栈打印
	while(s.isEmpty()==false)
	{
		cout<<s.top()<<" ";
		s.pop(); 
	}
}

 

6. merge两个已经有序的链表

归并排序的思想



ListNode* merge(ListNode* pHead1, ListNode* pHead2)
{
	if(pHead1==NULL)
		return pHead2;
	else if(pHead2==NULL)
		return pHead1;
		
	ListNode *neww = NULL; //新链表的头结点
	
	if(pHead1->key<pHead2->key) //先对新链表的头结点进行设置 
	{
		neww=pHead1;
		pHead1=pHead1->pNext;
	}
	else
	{
		neww=pHead2;
		pHead2=pHead2->pNext;
	}
	
	ListNode *neww_return = neww; //记录新链表的头结点,用于最后返回用 
	
	while(pHead1!=NULL && pHead2!=NULL)
	{
		if(pHead1->key<pHead2->key)
		{
			neww->pNext=pHead1;
			pHead1=pHead1->pNext;
			neww=neww->pNext;
		}
		else 
		{
			neww->pNext=pHead2;
			pHead2=pHead2->pNext;
			neww=neww->pNext;
		}
	}
	
	//还剩下的话,直接接上
	if(pHead1!=NULL)
		neww->pNext=pHead1;
	if(pHead2!=NULL)
		neww->pNext=pHead2;
	
	return  neww_return;
}

 

7. 判断一个单链表中是否有环

这里也是用到两个指针。如果一个链表中有环,也就是说用一个指针去遍历,是永远走不到头的。因此,我们可以用两个指针去遍历,一个指针一次走两步,一个指针一次走一步,如果有环,两个指针肯定会在环中相遇。时间复杂度为O(n)。

bool hasCircle(ListNode* pHead) //是否有环 
{
	ListNode* fast=pHead; //一次走两步 
	ListNode* slow=pHead; //一次走一步
	
	while(fast!=NULL && fast->pNext!=NULL)
	{
		fast=fast->pNext->pNext;
		slow=slow->pNext;
		//快慢指针相遇,说明有环 
		if(slow==fast)
			return true;
	} 
	
	//fast指针遍历结束了,说明链表并不是无止尽的,说明没有环 
	return false;
}

 

8. 判断两个单链表是否相交

如果两个链表相交于某一节点,那么在这个相交节点之后的所有节点都是两个链表所共有的。也就是说,如果两个链表相交,那么最后一个节点肯定是共有的(看图理解)。先遍历第一个链表,记住最后一个节点,然后遍历第二个链表,到最后一个节点时和第一个链表的最后一个节点做比较,如果相同,则相交,否则不相交。时间复杂度为O(len1+len2),因为只需要1or2个额外指针保存最后一个节点地址,空间复杂度为O(1)。

链表相交一定是下面这种形状的,不可能是x字交叉的那种,你想嘛,一个链表结点又不可能有两个pNext指针

bool isIntersected(ListNode* pHead1,ListNode* pHead2)  
{
	ListNode* p1=pHead1; 
	ListNode* p2=pHead2; 
	if(p1==NULL || p2==NULL)
		return false;
	
	//先找到链表1的尾结点
	while(p1->pNext!=NULL) //因为尾结点的pNext==NULL 
	{
		p1=p1->pNext;
	}
	//再找到链表2的尾结点
	while(p2->pNext!=NULL) //因为尾结点的pNext==NULL 
	{
		p2=p2->pNext;
	}
	if(p1==p2) //如果尾结点相同,说明相交
		return true;
	return false; 
}

 

9. 求两个单链表相交的第一个节点

刚开始我想得太简单了,觉得就是两个链表一起同时走,每走一步判断是否相同。

但是忘了考虑一点:两个链表并不是一样长。

比较完善的思路是:先判断是否相交(判断最后一个结点是否相同)——同时也是在计算两个单链表的长度,然后再让更长的的链表先走多出来的长度,这样两个链表当前节点到第一个相交节点的距离就相等了,然后一起向后遍历,直到两个节点的地址相同。

ListNode* firstIntersection(ListNode* pHead1,ListNode* pHead2)  
{
	ListNode* p1=pHead1; 
	ListNode* p2=pHead2; 
	if(p1==NULL || p2==NULL)
		return NULL;
	
	//判断是否相交 + 计算长度
	int nLength1=1;
	int nLength2=1; 
	//先找到链表1的尾结点
	while(p1->pNext!=NULL) //因为尾结点的pNext==NULL 
	{
		p1=p1->pNext;
		nLength1++;
	}
	//再找到链表2的尾结点
	while(p2->pNext!=NULL) //因为尾结点的pNext==NULL 
	{
		p2=p2->pNext;
		nLength2++;
	}
	if(p1!=p2) //如果尾结点相同,说明相交
		return NULL;
		
	//寻找第一个相交的结点
	if(nLength1>=nLength2)//第一个单链表更长
	{
		int len=nLength1 - nLength2;
		if(len>0)
		{
			len--;
			p1=p1->pNext;
		}
	}
	else //第二个单链表更长 
	{
		int len=nLength2 - nLength1;
		if(len>0)
		{
			len--;
			p2=p2->pNext;
		}
	} 
	
	//这时候两个链表当前指针都离相交的第一个结点距离相同
	while(p1!=p2)
	{
		p1=p1->pNext;
		p2=p2->pNext;
	} 
	return p1;
}

 

10. 已知一个单链表中存在环,求进入环中的第一个节点

对于有环的单链表,一个fast一个slow指针,等到二者相等,可以说明有环、遍历无止尽,也说明让二者相等的这个点在环上。这个时候,把这个点“解开”,可以得到如下图所示的两条相交的链表。(对于单链表,我认为很重要的就是要注意链表的方向!!!对编程很关键

这个时候,问题就转化为——求相交的第一个结点!

ListNode *detectCycle(ListNode *head) {
        //断掉环,来求两个单链表的公共交点
        
        //先判空和一个
        if(head==NULL)
            return NULL;
        if(head->next==NULL)
            return NULL;
        //先判有没有环——能不能相遇      
        ListNode* fast=head;
        ListNode* slow=head;
        int flag=0;
        while(fast->next!=NULL && fast->next->next!=NULL)
        {
            fast=fast->next->next;
            slow=slow->next;
            if(fast==slow)
            {
                flag=1; //相遇则说明有环
                break;
            }
        }
        if(flag==0)
            return NULL;
        //此时fast==slow并且是环上的一个点,以此断开变成两条相交的单链表
        ListNode *new1 = head;
        ListNode *new2 = slow->next;
        slow->next=NULL;
        
        //肯定相交的(无需判断最后一个结点是否相同),直接去找交点
        //先计算两个单链表的长度,各自走到离交点的相等距离处
        int len1=0,len2=0;
        ListNode *p1=new1;
        ListNode *p2=new2;
        while(p1!=NULL)
        {
            p1=p1->next;
            len1++;
        }
        while(p2!=NULL)
        {
            p2=p2->next;
            len2++;
        }
        
        p1=new1; //注意再回到开头
        p2=new2;
        if(len1<len2) 
        {
            p2=new2;
            int n = len2-len1;
            while(n--)
                p2=p2->next;
        }
        else if(len1>len2)
        {
            p1=new1;
            int n=len1-len2;
            while(n--)
                p1=p1->next;
        }
        //然后一起共同往前,直到相同即可
        while(p1!=p2)
        {
            p1=p1->next;
            p2=p2->next;
        }
        return p1;
    }

11. 给出一单链表头指针pHead和一结点指针pToBeDeleted,O(1)时间复杂度删除结点pToBeDeleted

一般我们认为,单链表中的删除操作的时间复杂度是O(n),是因为要首先找到该结点的头结点,然后把p->next=p->next->next,然后free(pToBeDeleted)。

但是,由于单链表的每个节点的结构是一样的(标志其实就是一个值而已),所以其实可以把下一个结点的值复制到这个结点,然后删除下一个结点。这样的话,给出pToBeDeleted指针,只需时间复杂度O(1)

但是,如果pToBeDeleted是最后一个结点,就没办法了,只能从头遍历找到倒数第二个结点,需O(n)。但平均来说,还是O(1)

void deletep(ListNode*head,ListNode* pToBeDeleted)
{
	if(head==NULL) //如果链表为空 
	{
		return;
	}
	if(pToBeDeleted->pNext!=NULL) //不是最后一个结点
	{
		pToBeDeleted->key = pToBeDeleted->pNext->key;
		ListNode* pp = pToBeDeleted->pNext;
		pToBeDeleted->pNext = pToBeDeleted->pNext->pNext;
		free(pp);
	} 
	else //是最后一个结点,只能遍历找前一个 
	{
		if(head->pNext==NULL) //该链表只有一个结点(那还找什么前一个结点啊
		{
			head=NULL;
			free(head);
		} 
		ListNode* pp = head;
		while(pp->pNext!=pToBeDeleted)
			pp=pp->pNext;
		pp->pNext=NULL;
		free(pToBeDeleted);
	} 
}

我的天啊...我怎么还是忘了考虑“链表为空”“链表中只有一个结点”的情况呢,以后关于链表的题,最后一定要检查下这两个情况有没有做考虑!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值