顺序表、单链表经典算法题分享(未完待续...)

顺序表:
①移除元素;
②合并两个有序数组。
单链表:
①移除链表元素;
②反转链表;
③合并两个有序链表;
④寻找链表的中间节点;

(待续......)

⑤环形链表的约瑟夫问题;

⑥分割链表。

(一)顺序表算法题

(1)移除元素

链接:https://leetcode.cn/problems/remove-element

在不创建新数组的情况下实现val元素的移除,我们采用双指针法

让源指针循环遍历所有下标元素,当元素为val时,是我们需要移除的,我们就不管了,让其等着被其他值覆盖(或者不在访问范围内),只让遍历的src++;当元素不为val时,是"新数组"需要的元素,就将nums[dst]=nums[src],赋值完后再dst++,src++,继续向下。

等遍历完成时,以dst为分界线,以[0,dst)为下标的元素构成的数组就是我们需要的"新数组",而以[dst,numsSize)为下标的元素不在访问之列,返回值就是dst。

如图:

复制代码
int removeElement(int* nums, int numsSize, int val) {
    int src = 0;
    int dst = 0;
    for(src = 0; src<numsSize ; src++)
    {
        if(val != nums[src])
        {
            nums[dst] = nums[src];
            dst++;
        }

    }
    //遍历完成
    return dst;
}

注意:此方法叫"双指针法",但src、dst都是数组的下标数字,不是真正的指针变量。

(2)合并两个有序数组

链接:https://leetcode.cn/problems/merge-sorted-array

思路①

------先将num2中数据放入num1数组的后面,再使用排序算法使其呈递增排列。

复制代码
void merge(int* nums1, int nums1Size, int m, int* nums2, int nums2Size, int n) {
    int i = 0;
    for(i=0;i<n;i++)
    {
        //塞到num1的后面
        nums1[i+m] = nums2[i];
    }
    //冒泡排序
    i = 0;
    for(i=0;i<n+m-1;i++)
    //趟数
    {
      int j = 0;
      for(j = 0;j<m+n-1-i;j++)
      //每一趟都能排好一个数据的位置,比较的次数自然就能-i
      {
          if(nums1[j]>nums1[j+1])
          //递增
          {
            int tmp = nums1[j];
            nums1[j] = nums1[j+1];
            nums1[j+1] = tmp;
          }

      }
    }
}

但冒泡排序嵌套的for循环效率不高,我们看看思路②。

思路②

------从后向前进行比较后,再从后向前将数据元素放入num1数组的后方。

复制代码
void merge(int* nums1, int nums1Size, int m, int* nums2, int nums2Size, int n) {
    //定位到后方
    int l1 = m-1;
    int l2 = n-1;
    int l3 = m+n-1;
    while(l1>=0 && l2>=0)
    {
        //升序
        if(nums1[l1]>nums2[l2])
        {
            nums1[l3--] = nums1[l1--];
        }
        else
        {
            nums1[l3--] = nums2[l2--];
        }
    }
    //处理l1<0但l2>=0的情况
    //注意是while(l2>=0),而不是if(l2>=0)
    while(l2>=0)
    {
        nums1[l3--] = nums2[l2--];
    }

}

(二)单链表算法题

(1)移除链表元素

链接:https://leetcode.cn/problems/remove-linked-list-elements

思路①

------遍历链表,当遇到val节点时就移除。

分支语句为三类:头结点的移除、非头结点的移除以及不移除

复制代码
struct ListNode* removeElements(struct ListNode* head, int val)
 {
    //思路:遍历原链表,将值为val的节点移除
    struct ListNode* pcur = head;
    struct ListNode* prev = head;
    while (pcur)
   {
      //头结点的移除
	  if (pcur == head && pcur->val == val)
	  {
		struct ListNode* next = pcur->next;
		free(pcur);
		pcur = next;
		head = pcur;
	  }
	  else if ( pcur->val == val)
	  {
		prev->next = pcur->next;
	    free(pcur);
		pcur = prev->next;
	  }
	  else
	  {
		prev = pcur;
		pcur = pcur->next;
	  }
    }

    return head;
}

把头结点和非头结点的移除分成两种情况,是因为在头结点的移除中,我们需要改变head的值,而无论是非头结点还是不移除的情况下,头结点head的值是不变的。

错误呈现:
复制代码
struct ListNode* removeElements(struct ListNode* head, int val) {
    //思路:遍历原链表,将值为val的节点移除
    struct ListNode* pcur = head;
    struct ListNode* prev = head;
    while (pcur)
   {
      //头结点的移除
	  if (pcur == head && pcur->val == val)
	  {
		struct ListNode* next = pcur->next;
		free(pcur);
		pcur = next;
		head = pcur;
	  }
	  if ( pcur->val == val)
	  {
		prev->next = pcur->next;
	    free(pcur);
		pcur = prev->next;
	  }
	  else
	  {
		prev = pcur;
		pcur = pcur->next;
	  }
    }

    return head;
}

本来应该是if......else if......else三个并列的分支语句,只会选择三种情况中的一种进入,但换成if....../if......else,这就是两个并列的分支语句了,会先判断是否要if,在判断是否要进入if......else......二者中的一者。

当链表中所有元素都需要移除时,if....../if......else的写法就会报错。在第一个if中,链表的所有节点都移除了,pcur=NULL,再进入第二个分支if......else......中时,在if的条件判断(pcur->val == val)中,要对pcur进行访问,但此时pcur已经为空了。

而对空指针进行访问是错误操作。

思路② (⭐)

------创建新链表,尾插值为val的节点到新链表中。

代码实现:
复制代码
typedef struct ListNode ListNode;
struct ListNode* removeElements(struct ListNode* head, int val) 
{
    ListNode* newHead = NULL;
    ListNode* newTail = NULL;
    //"新链表"的头结点、尾结点
    ListNode* pcur = head;
    while(pcur)
    {
        //需要尾插时
        if(pcur->val != val)
        {
             //新链表为空时
             if(newHead == NULL)
             {
                newHead = newTail = pcur;
                pcur = pcur->next;
                newTail->next =NULL;

             }
             else
             //链表不为空
             {
                newTail->next = pcur;
                newTail = pcur;
                pcur = pcur->next;
                newTail->next =NULL;
 
             }
        }
        //不需要尾插时
        else
        {
            pcur = pcur->next;
        }
      
    }
    return newHead;
}
思路详解:

虽然方法叫"创建新链表法",但此处并没有真正动态申请内存,仅仅只是创建了两个指针newHead和newTail,我们只是把需要保留的节点串在newHead和newTail之间(可以多个指针指向同一节点),形成新链表(链表是新的,但节点不是新的。)

起初,我们创建指针newHead和newTail,并置空,在创建pcur作为遍历原链表的指针,在第一次遇到pcur->val != val时,newHead和newTail都要指向pcur,且newTail->next要置空,继续遍历,pcur=pcur->next。

当遇到pcur->val == val时,不需要放入newHead和newTail之间,那就继续遍历,让pcur=pcur->next。如果又遇见pcur->val != val,newHead作为头结点,不需要改变,newTail是尾指针,要再次赋值,使newTail = pcur,但仅仅只是改动newTail的指向还不够。newHead指向原先的某次pcur,而newTail指向最新的pcur,首尾指针还是各自为营,并没有串起来,在使newTail = pcur之前,我们应该让newTail->next = pcur 。(第一次插入后,newTail也是新链表的头结点指针,通过newTail->next,我们能让头结点的后继指针,指向新的要被新插入的节点。)再将newTail->next置空。

※注意:此处不能写newHead->next = pcur!

虽然newHead->next = pcur也能让头结点的后继指针,指向新的要被插入的节点,但从此以后,新链表只会有两个节点------头节点和最后一次被插入的节点。

newTail->next可以让每个前一次被插入节点的后继指针指向要被新插入的节点,但newHead->next每次都只能让头结点指向新插入的节点,如果中间还有被插入的节点,都会被忽略。

错误呈现:①②③

①newHead->next = pcur

复制代码
typedef struct ListNode ListNode;
struct ListNode* removeElements(struct ListNode* head, int val) {
    ListNode* newHead = NULL;
    ListNode* newTail = NULL;
    //"新链表"的头结点、尾结点
    ListNode* pcur = head;
    while(pcur)
    {
        //需要尾插时
        if(pcur->val != val)
        {
             //新链表为空时
             if(newHead == NULL)
             {
                newHead = newTail = pcur;
                pcur = pcur->next;
                newTail->next =NULL;

             }
             else
             //链表不为空
             {
                newHead->next = pcur;
                newTail = pcur;
                pcur = pcur->next;
                newTail->next =NULL;
 
             }
        }
        //不需要尾插时
        else
        {
            pcur = pcur->next;
        }
      
    }
    return newHead;
}

注意事项里已经分析了,此处只是贴出错误代码,不再赘述。

②能将newTail->next =NULL;写在循环结束后吗?

复制代码
typedef struct ListNode ListNode;
struct ListNode* removeElements(struct ListNode* head, int val) {
    ListNode* newHead = NULL;
    ListNode* newTail = NULL;
    //"新链表"的头结点、尾结点
    ListNode* pcur = head;
    while(pcur)
    {
        //需要尾插时
        if(pcur->val != val)
        {
             //新链表为空时
             if(newHead == NULL)
             {
                newHead = newTail = pcur;
                pcur = pcur->next;

             }
             else
             //链表不为空
             {
                newTail->next = pcur;
                newTail = pcur;
                pcur = pcur->next;
             }
        }
        //不需要尾插时
        else
        {
            pcur = pcur->next;
        }
      
    }
    //能将newTail->next =NULL;写在循环结束后吗?
    newTail->next =NULL;
    return newHead;
}

提交之后,会报错------

当原链表为空时,pcur为空,进不去循环,直接执行newTail->next = NULL,可此时newTail也为空,对空指针进行访问是错误操作,如果我们想在循环结束之后再让newTail->next置空,需要先对newTail判空。(不能对pcur判空,无论原链表是否为空链表,执行到newTail->next = NULL时,pcur都为空)

正确代码:

复制代码
typedef struct ListNode ListNode;
struct ListNode* removeElements(struct ListNode* head, int val) {
    ListNode* newHead = NULL;
    ListNode* newTail = NULL;
    //"新链表"的头结点、尾结点
    ListNode* pcur = head;
    while(pcur)
    {
        //需要尾插时
        if(pcur->val != val)
        {
             //新链表为空时
             if(newHead == NULL)
             {
                newHead = newTail = pcur;
                pcur = pcur->next;

             }
             else
             //链表不为空
             {
                newTail->next = pcur;
                newTail = pcur;
                pcur = pcur->next;
             }
        }
        //不需要尾插时
        else
        {
            pcur = pcur->next;
        }
      
    }
    if(newTail)
       newTail->next =NULL;
    return newHead;
}

③能将else里的如下两条语句的位置进行颠倒吗?

复制代码
pcur = pcur->next;
newTail->next =NULL;

typedef struct ListNode ListNode;
struct ListNode* removeElements(struct ListNode* head, int val) {
    ListNode* newHead = NULL;
    ListNode* newTail = NULL;
    //"新链表"的头结点、尾结点
    ListNode* pcur = head;
    while(pcur)
    {
        //需要尾插时
        if(pcur->val != val)
        {
             //新链表为空时
             if(newHead == NULL)
             {
                newHead = newTail = pcur;
                pcur = pcur->next;
                newTail->next =NULL;

             }
             else
             //链表不为空
             {
                newTail->next = pcur;
                newTail = pcur;
                newTail->next =NULL;
                pcur = pcur->next; 不能调转两条语句的位置
 
             }
        }
        //不需要尾插时
        else
        {
            pcur = pcur->next;
        }
      
    }
    return newHead;
}

提交后,输出结果有误------

我在VS2022里调试之后,可以发现在在第二次插入2时会进入else分支语句,newTail->next = pcur;和newTail = pcur;都没问题,但我们执行newTail->next =NULL;时,是将pcur节点的后继指针置空了,等我们再执行pcur = pcur->next;时,就是将NULL赋给pcur了,就会跳出循环。

原先的顺序中,我们是先将pcur->next赋给pcur,再执行newTail->next =NULL;的,所以不会影响到pcur的值的变动。

所以颠倒语句顺序就是错的,这么看来,似乎将newTail->next =NULL;写在循环结束后,反而更方便?不过也要注意好对newTail判空。

(2)反转链表

链接:https://leetcode.cn/problems/reverse-linked-list

思路①

------创建count数出链表的节点个数,反转 count/2 次。

思路比较清晰,但实现却很复杂,我们需要让第一个节点和最后一个节点进行数据交换,再从两头靠拢,逐步将两头的节点数据一一交换,总共要反转count/2次。

从头向中间靠拢的节点数据交换是很容易的,只需要借助next指针向下走就行,但从后往中间靠拢的节点数据交换就很麻烦。单链表是单向的,next指针只能指向后一个节点,如果想找到前一个节点就需要额外处理,可这又怎么处理------麻烦!

思路②(⭐)

------创建新链表,将原链表节点一个个头插进新链表中。

这个思路和移除链表元素的第二个思路很像,只不过移除链表元素我们用的是遍历+尾插,而此处,我们用遍历+头插,而且思路更简单,反转链表中,原链表中的所有元素都需要头插,并不需要再进行数据判断后决定是否要插入。

依旧先明白,此处也只是创建了两个新节点指针,而不是真正创建了新节点。

既然要遍历,那就需要一个遍历指针pcur,当newHead和newTail还为空时,我们要做的是将pcur赋值给newHead和newTail,再将newTail->next置空,pcur继续向下走,但此处就出了问题。

我们之所以能通过pcur找到原链表的下一个指针,是因为pcur对应节点的next指针指向了下一个节点,但是我们在"newTail->next置空"语句中,让pcur节点的next指针发生了变化,pcu->next指向的不再是下一个指针了。

由此提醒我们,需要提前将pcur->next保存起来(创建临时变量next),无论是何时的插入都需要保存pcur->next和pcur遍历向下,所以我们将保存语句和pcur遍历语句写在while循环内、if......else语句外。

插完第一个节点后,newHead、newTail不再为空。

再进行插入时,newHead变动成新的pcur,要想让新链表能成功被串起来,就必须要让新的newHead的next指针指向上一个被插入的节点。

可上一个被插入的节点是什么?是原先的pcur?还是newTail?

是原先的pcur,但pcur已经遍历向下了,我们又去哪里找到原先的pcur呢?

这就又提醒我们,需要将每次插入节点的上一个节点保存下来(创建临时变量prev)。

回到再进行插入时,newHead变成了新的pcur,newHead->next变成prev,而prev被使用后,应该随着pcur向下走,也变成新的pcur,然后再让pcur遍历向下,这样prev和pcur就永远是前后关系。

代码实现:
复制代码
typedef struct ListNode ListNode;
struct ListNode* reverseList(struct ListNode* head) 
{
ListNode* newTail = NULL;
ListNode* newHead = NULL;
ListNode* prev = head;
//存储老pcur,让newHead->next = prev
ListNode* pcur = head;
//遍历指针
ListNode* next = NULL;
//储存新pcur的next指针,让改动后,仍能让pcur = next
while (pcur)
{
	//第一次头插
    next = pcur->next;
    // ListNode* next = head->next;
	if (newHead == NULL)
	{
		newHead = newTail = pcur;
		newTail->next = NULL;
		// pcur = next;
	}
	//非空头插
	else
	{
		newHead = pcur;
		newHead->next = prev;
		prev = pcur;
		// pcur = next;
	}
	pcur = next;
    
}
    return newHead;
}

虽然只有短短不到三十行代码量,但却想理清,也并不容易,我们来看几种常见错误。(实则就是我写的时候犯的诸多错误......)

错误呈现:①②③

①忘记要存pcur->next(没有创建临时指针变量next)

复制代码
SLTNode* reverseList(SLTNode* head) 
{
	SLTNode* pcur = head;
	SLTNode* newHead = NULL;
	SLTNode* newTail = NULL;
	SLTNode* prev = head;
	while (pcur)
	{
		//头插
		if (newHead == NULL)
		{
			newHead = newTail = pcur;
			newTail->next = NULL;
			pcur = pcur->next;
		}
		else
		{
			newHead = pcur;
			newHead->next = prev;
			prev = pcur;
			pcur = pcur->next;
		}
	}
	return newHead;
}

②在循环外写 ListNode* next = head->next;

复制代码
typedef struct ListNode ListNode;
struct ListNode* reverseList(struct ListNode* head) 
{
ListNode* newTail = NULL;
ListNode* newHead = NULL;
ListNode* prev = head;
ListNode* pcur = head;
ListNode* next = head->next;
while (pcur)
{
	//第一次头插
   next = pcur->next;
   if (newHead == NULL)
	{
		newHead = newTail = pcur;
		newTail->next = NULL;
	}
	//非空头插
	else
	{
		newHead = pcur;
		newHead->next = prev;
		prev = pcur;
	}
	pcur = next;    
}
    return newHead;
}

当原链表为空时,直接让next=head->next,就是对空指针进行访问,错误操作。

③在循环结束后才将newTail->next置空

复制代码
typedef struct ListNode ListNode;
struct ListNode* reverseList(struct ListNode* head) 
{
ListNode* newTail = NULL;
ListNode* newHead = NULL;
ListNode* prev = head;
ListNode* pcur = head;
ListNode* next = NULL;
while (pcur)
{
	//第一次头插
   next = pcur->next;
   if (newHead == NULL)
	{
		newHead = newTail = pcur;
	}
	//非空头插
	else
	{
		newHead = pcur;
		newHead->next = prev;
		prev = pcur;
	}
	pcur = next;    
}
    newTail->next = NULL;
    return newHead;
}

和第②的错误呈现的原因一致,当原链表为空时,不进入循环,newTail本身就为空指针,newTail->next的操作,同样是访问了空指针,错误操作。

思路③

------创建三个临时指针变量,遍历改变原链表的指针指向

代码实现:
复制代码
typedef struct ListNode ListNode;
struct ListNode* reverseList(struct ListNode* head) {
    if(head == NULL)
      return head;
    ListNode* l1 = NULL;
    ListNode* l2 = head;
    ListNode* l3 = head->next;
    ListNode* newHead = NULL;
    ListNode* newTail = NULL;
    while (l3)
  {
	l2->next = l1;
	l1 = l2;
	l2 = l3;
	l3 = l3->next;
  }
   //漏了一个尾结点
   l2->next = l1;
   return l2;
}
思路详解:

与第二种思路相比,只是说没有把创建新头尾节点摆到明面上来说,但是思想是一致的,都是遍历原链表,改变各个节点的next指针的指向,从让原链表反着来。

创建三个临时指针变量l1、l2、l3,l1指向空,l2指向原链表的头结点,l3指向头结点的下一个节点,基本逻辑是让l2的next指针指向l1,再将l2赋给l1,再将l3赋给l2,最后让l3向后走,l3=l3->next,遍历完原链表,从而实现链表的反转。

细节答疑:

①为什么循环的条件是 l3 ?

先把循环条件当做l3,在循环的最后一轮,l1指向了倒数第二个节点,l2指向了尾结点,l3则是指向了NULL,如果此时循环还继续,那么l1会指向尾结点,l2会指向NULL,l3=l3->next,可是此时l3已经为空了,访问空指针,错误操作。

说明,当l3为空时,就应该要跳出循环。

②为什么要额外讨论空链表的情况?

在定义l3是,初始值为head->next,如果不额外讨论空链表的情况,又会出现访问空指针的操作错误。

③为什么在循环结束后,还会漏掉一个节点?

循环的最后一轮时,指向倒数第二个节点的l2的next指针指向了倒数第三个节点l1,然后,新的l1指向了倒数第二个节点,新的l2指向了尾结点,l3则是指向了NULL,我们还没来得及让新的l2->next指向新的l1(没来得及让尾结点的next指针指向倒数第一个节点),所以说漏了处理一个节点的问题。

让其l2->next = l1即可。

并且,l2指向的尾结点就是反转后链表的头结点,所以返回值是l2。

(3)合并两个有序链表

链接:合并两个有序链表

思路①

------创建新链表,设立新的头尾指针newHead,newTail,进行遍历比较,尾插组成新链表。

尾插的关键在于第一要分空链表和非空链表的情况,第二要先让原尾结点的next指针指向新的插入节点之后,再调整newTail的指向(不然无法使尾插的节点相连构成新链表)

复制代码
    if(list1 == NULL)
      return list2;
    if(list2 == NULL)
      return list1;
    ListNode* newHead = NULL;
    ListNode* newTail = NULL;
    ListNode* l1 = list1;
    ListNode* l2 = list2;
    while(l1 != NULL && l2 != NULL)
    {
        if(l1->val < l2->val )
        //头插
        {
            if( newHead == NULL)
            {
               newHead = newTail = l1;
            //    newTail->next = NULL;
                
            }
            else
            {
                newTail->next = l1;
                newTail = l1;
            }
            l1 = l1->next;
        }
        else
        {
           if( newHead == NULL)
            {
               newHead = newTail = l2;
               // newTail->next = NULL;
            }
            else
            {
               newTail->next = l2;
               newTail = l2;
            }
            l2 = l2->next;         
        }
        
    }
    //l1 == NULL l2 == NULL
    if(l1 == NULL)
    {
        newTail->next = l2;
    }
    // else 可行
    if(l2 == NULL)
    {
        newTail->next = l1;
    }
    return newHead;

错误呈现:

①当一个链表为空返回另一个链表的头指针的逻辑,不是if.....else......。

②不要在头插时,让newTail->next置空,会改变l1或是l2的next指针指向,丢失了后面紧跟着的数据。

思路②

(4)寻找链表的中间节点

链接:寻找链表的中间节点

思路①(经典思路)

------快慢指针,2*slow = fast,当fast走到尾,slow就找到了中间节点。

注意:寻找偶数个节点的链表的中间节点,指的是找中间两个的后一个节点。

复制代码
typedef struct ListNode ListNode;
struct ListNode* middleNode(struct ListNode* head) {
     //快慢指针
     ListNode* fast = head;
     ListNode* slow = head;
     while(fast && fast->next)
     {
        fast = fast->next->next;
        slow = slow->next;
     }
     //slow就能找到中间节点
     return slow;

}

(5)环形链表的约瑟夫问题

链接:环形链表的约瑟夫问题

(6)分割链表

链接:分割链表

------未完待续------

相关推荐
qyzm2 小时前
牛客周赛 Round 140
数据结构·python·算法
我不是懒洋洋2 小时前
【经典题目】栈和队列面试题(括号匹配问题、用队列实现栈、设计循环队列、用栈实现队列)
c语言·开发语言·数据结构·算法·leetcode·链表·ecmascript
锅挤2 小时前
数据结构复习(第七章):查找
数据结构
j_xxx404_2 小时前
用系统调用从零封装一个C语言标准I/O库 | 附源码
linux·c语言·开发语言·后端
Polaris_T2 小时前
2026最新字节大模型岗面经汇总(多平台整理)
人工智能·经验分享·算法·aigc·求职招聘
Xiaoᴗo.3 小时前
C语言2.0---------
c语言·开发语言·数据结构
ghie90903 小时前
MATLAB 解线性方程组的迭代法
开发语言·算法·matlab
m0_743106463 小时前
【浙大&南洋理工最新综述】Feed-Forward 3D Scene Modeling(二)
人工智能·算法·计算机视觉·3d·几何学
Java_小白呀3 小时前
考研408数据结构(栈与队列)
数据结构·考研·栈和队列·考研408