链表——常规操作

单链表反转

方法一:

考虑如何将一个单链表反转,一开始的灵感是由头插法建立单链表算法而来的,即事先建立一个空的单链表,在遍历单链表的同时,不断的将结点从头插入到空链表中,这样一来,就建立出了单链表的反转链表。

算法思路:

  • 建立新的空链表,令指针q指向空链表的头节点
  • 遍历单链表,提取每次访问结点的数据域
  • 创建新的Node结点,将提取的结点data数据赋值给新结点的data数据域
  • 令新结点的指针指向头节点的后继结点,再将头节点的后继指针指向新结点,直到遍历结束

时间复杂度O(n),空间复杂度O(n)

具体实现代码如下:

List list;                    // 已知链表list
List new_list;                // 新建空链表new_list
Node *p = list.head();
Node *q = new_list.head();
while (p->next != NULL)       // 遍历链表list
{
    Node *s = new Node;       // 建立新的结点并初始化
    s->data = p->data;
    s->next = q->next;        // 将新结点插入到空链表首部
    head->next = s;
    p = p->next; 
}

这样便得到了新的链表new_list,即原链表list的反转链表。

方法二:

在头插法创建新链表的基础上,设计出在链表本身上进行的原地反转算法。

算法思路:

  • 采用三个结点指针,head、prev、pcur,分别指向头节点,处理结点的前继结点,待处理结点
  • 以第二个结点开始作为待处理结点,进行反转
  • 令prev的后继结点指向pcur的后继结点,再令pcur的后继结点指向head的后继结点,令head的后继结点指向pcur结点,最后重新指定新的pcur结点为prev的后继结点
  • 重复第三步,直至待处理结点为空

时间复杂度O(n),空间复杂度O(1)

具体实现代码如下:

void MyList::ReverseList()
{
	if (head->next == NULL || head->next->next == NULL)    // 特殊情况处理
		return;
	Node *prev = head->next;    // 初始化反转结点的前继指针
	Node *pcur = prev->next;    // 初始化需要反转的结点指针
	while (pcur != NULL)
	{
		prev->next = pcur->next;    
		pcur->next = head->next;
		head->next = pcur;
		pcur = prev->next;
	}
}

两个有序的链表合并

首先我们分析两个有序链表,这里举例 list1 = { 1, 3, 5, 7 }, list2 = { 2, 4, 6, 8 }。其中pHead1pHead2分别指向两个链表的首结点,接着我们分析两个链表合并后的头指针pMergedHead,通过比较两个链表的头结点的data值可知,list1链表的头结点data值小于list2的头结点data值,因此确定出合并后链表的头指针为pHead1。紧接着确定list1的子链表与list2链表合并后的头指针,依次递归,直至两个链表中任意链表为空。

因此总结出递归算法的思路:

  • 每次递归过程返回指向两个链表合并后的头结点的指针pMergedHead
  • 令上次递归得到的pMergedHead指针指向此次递归过程的结果

由于每次递归都会使得两条链表中的一条减少一个结点,因此当某条链表为空时,会返回另一条链表的头结点,此时到达递归的基准情形

时间复杂度O(min(m, n)),空间复杂度O(min(m, n))

具体代码实现如下:

Node* Merge(Node* pHead1, Node* pHead2)
{
    if (pHead1 == NULL)        // 递归的基准情形
        return pHead2;
    else if (pHead2 == NULL)
        return pHead1;
 
    Node* pMergedHead = NULL;  // 定义合并后链表的头指针
 
    if (pHead1->data < pHead2->data)    // 分情况递归
    {
        pMergedHead = pHead1;
        pMergedHead->next = Merge(pHead1->next, pHead2);
    }
    else
    {
        pMergedHead = pHead2;
        pMergedHead->next = Merge(pHead1, pHead2->next);
    }
 
    return pMergedHead;
}

快慢指针的应用

由于快慢指针在链表的一些操作中有很大的帮助,这里先介绍一下快慢指针的概念。

快慢指针中的快慢指的是移动的步长,即每次向前移动速度的快慢。例如可以让快指针每次沿链表向前移动2,慢指针每次向前移动1次。我们在链表的某些操作时,可以定义两个指针来遍历链表,通过使两个指针的移动速度不同,来使操作变得更加简单。

快慢指针应用(一):判断链表是否存在环

如果链表存在环,就好像操场的跑道是一个环形一样。此时让快慢指针都从链表头开始遍历,快指针每次向前移动两个位置,慢指针每次向前移动一个位置;如果快指针到达NULL,说明链表以NULL为结尾,没有环。如果快指针追上慢指针,则表示有环。

时间复杂度O(n),空间复杂度O(1)

具体代码实现如下:

bool HasCircle(Node *head)
{
  if (head == NULL)
    return false;
  Node *slow = head, *fast = head;
  while (fast != NULL && fast->next!=NULL)    // 注意循环结束的条件 
  {
     slow = slow->next;         // 慢指针每次前进一步
     fast = fast->next->next;   // 快指针每次前进两步
     if(slow == fast)           // 相遇,存在环
         return true;
  }
  return false;
}

快慢指针应用(二):判断链表是否存在环,如果存在,找到环入口

之前已经介绍了如何判断链表是否存在环,接下来分析如果链表存在环,如何找到环的入口点?

当fast若与slow相遇时,slow肯定没有走遍历完链表或者恰好遍历一圈。于是我们从链表头与相遇点分别设一个指针,每次各走一步,两个指针必定相遇,且相遇第一点为环入口点。(这里有必要给出图文解释,以后会补充)

时间复杂度O(n),空间复杂度O(1)

具体代码实现如下:

Node* findLoopPort(Node *head) 
{
    Node *fast, *slow;
    fast = slow = head;
    
    // 第一步:判断链表是否存在环
    while (fast && fast->next) 
    {  
        slow = slow->next;
        fast = fast->next->next;
        if (slow == fast)                          // 链表存在环
        {
            break;
        }
    }
    if ((fast == NULL) || (fast->next == NULL))    // 链表不存在环
    {                                          
        return NULL;
    }

    // 第二步:寻找环的入口点
    slow = head;                    // 让slow回到链表的起点,fast留在相遇点
    while (slow != fast)            // 当slow和fast再次相遇时,那个点就是环的入口点
    {
        slow = slow->next;          // 此时快慢指针每次都只走一步
        fast = fast->next;
    }
    return slow;
}

删除链表中倒数第n个结点

可以定义两个指针,第一个指针从链表的头指针开始遍历向前走k-1步,第二个指针保持不动;从第K步开始,第二个指针也开始从链表的头指针开始遍历。由于两个指针的距离保持在k-1,当第一个指针到达链表的尾节点时候,第二个指针正好是倒数第K个节点。

时间复杂度O(n),空间复杂度O(1)

具体代码实现如下:

bool RDelete_N(Node *pHead, unsigned n)      // 函数名前面的R代表反向  
{
	if (n == 0 || pHead == NULL)     // 这里k的计数是从1开始的,若k为0或链表为空返回NULL  
		return NULL;

	Node * pAhead = pHead;           // 初始化快指针和慢指针
	Node * preBehind = pHead;

	for (int i = 0; i < n; ++i)
	{
		pAhead = pAhead->next;
		if (pAhead == NULL)
			return false;            // 当链表长度小于n时候,返回false  
	}

	while (pAhead->next != NULL)     // 前后两个指针一起向前走,直到前面的指针指向尾结点  
	{
		preBehind = preBehind->next;
		pAhead = pAhead->next;
	}
    
    // 此时指针preBehind指向倒数第n个结点的前继结点
    // 开始删除操作

	Node *pBehind = preBehind->next;
	preBehind->next = pBehind->next;
	delete pBehind;

	return true;
}

求链表的中间结点

令慢指针每次移动一个位置,而快指针每次移动两个位置,由于快指针移动速度是慢指针的两倍,所以当慢指针指向链表尾结点时,慢指针刚好走到链表的中间。但是根据链表结点的个数要进行分类讨论(下面讨论以链表不存在头结点为前提):

  • 当链表结点个数为奇数时

此时快指针走到尾结点为终止条件,直接返回慢指针即可。

  • 当链表结点个数为偶数时

此时快指针达到倒数第二个结点为终止条件,我们这里选择返回链表中间两个结点的前面那个。

时间复杂度O(n),空间复杂度O(1)

具体代码实现如下:

Node* GetMiddle(Node *head)
{
    Node *fast, *slow;        // 初始化快慢指针指向第一个结点
    fast = slow = head;

    while (fast && slow)
    {
      if (fast->next == NULL)
          return slow;
      else if (fast->next != NULL && fast->next->next == NULL)
          return slow;      // 直接返回slow指针,另一种情况则是:return slow->next;
      else
      {
          fast = fast->next;
          fast = fast->next;
          slow = slow->next;
      }
    }
}

 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值