数据结构修炼——顺序表和链表的OJ题练习
目录
- 一、顺序表相关OJ题
- 1 移除元素
- 题目解析
- 2 合并两个有序数组
- 题目解析
- 二、链表相关OJ题
- 1 移除链表元素
- 题目解析
- 2 反转链表
- 题目解析
- 3 链表的中间结点
- 题目解析
- 4 合并两个有序链表
- 题目解析
- 5 链表的回文结构
- 题目解析
- 6 相交链表
- 题目解析
- 7 环形链表的判断
- 题目解析
- 8 环形链表
- 题目解析
一、顺序表相关OJ题
顺序表的实现与数组息息相关,不可分割,一些顺序表相关的算法实际上就是数组相关的算法,因此顺序表的灵活运用需要我们更深一步的理解与掌握数组。
1 移除元素
leetcode 27. 移除元素
题目解析
题目大意:
给定一个值val
,数组中不等于val
的元素个数为k(k >= 0 && k <= numsSize),我们需要将数组中所有等于val
的元素删除或者移到数组后面,并求出k值返回。
代码示例如下:
int removeElement(int* nums, int numsSize, int val)
{
int l1, l2;
l1 = l2 = 0;
while (l2 < numsSize)
{
if (nums[l2] == val)
{
l2++;
}
else
{
nums[l1] = nums[l2];
l1++;
l2++;
}
}
return l1;
}
这里给出的思路类似双指针法,定义两个变量代表下标,其中一个下标代表前k个元素的下标,其中一个下标经过等于val
的元素就跳过,不等于就赋值给前k个元素。
如图:
- 开始时
L1
,L2
都位于下标0,进入第一次循环,nums[L2]
不等于val
则赋值给nums[L1]
。且L1
,L2
加1,移动至下标1。 - 进入第二次循环,
nums[L2]
等于val
,L2
加1。L1
仍位于下标1,L2
位于下标2。 - 进入第三次循环,
nums[L2]
不等于val
,nums[L2]
赋值给nums[L1]
。L1
,L2
加1,L1
位于下标2,L2
位于下标3。
此时
以此类推…非val
值元素会按顺序逐个向前覆盖,直到L2
遍历完数组。
2 合并两个有序数组
leetcode 88.合并两个有序数组
题目解析
题目大意:合并两个非递减顺序的数组,两数组有效元素个数不同,合并数组也为非递减顺序且其结果存储在nums1数组中,因此nums1Size = m + n, nums2Size = n
。
代码示例如下:
void merge(int* nums1, int nums1Size, int m, int* nums2, int nums2Size, int n)
{
int L1 = m - 1;//nums1数组最后一个有效元素的下标
int L2 = n - 1;//nums2数组最后一个有效元素的下标
int L3 = m + n - 1;//nums1数组可用的最大下标
while (L1 >= 0 && L2 >= 0)//nums1或nums2数组遍历完就结束
{
//从后往前比较nums1与nums2中有效元素的大小,大的放到nums1[L3],小的继续比
if (nums1[L1] < nums2[L2])
{
nums1[L3--] = nums2[L2--];
}
else
{
nums1[L3--] = nums1[L1--];
}
}
while (L2 >= 0)//如果nums2数组中有剩余元素则有序放入nums1中,如果没有则L2<0,不进入循环
{
nums1[L3--] = nums2[L2--];
}
}
需要注意的是nums1数组因为存储合并结果,其容量为m + n,因此其后n个空间是空置的。这里采用的是从后往前比较两数组中的有效元素,较大的从后往前放置在nums1中。
由于数组为非递减序列的有序数组,因此在同一数组中靠后的元素一定大于等于前一元素,我们只需要比较两个数组的元素,判断出哪个更大即可。
例如:nums1[7] = 11 < nums2[5] = 12
,所以将12
放到nums1[13]
即可。接着继续拿nums1[7] = 11
比较nums2[4] = 10
,11
大所以放到nums1[12]
,以此类推…
最终可能有两种情况,一是nums1先比完所有元素,二是nums2先比完所有元素。如果是情况二,正好完成数组合并且合并结果存储在nums1数组中;如果是情况一,我们需要将nums2数组中剩余元素继续从后往前依次放置到nums1中。
该算法最终时间复杂度为O(m+n),m、n分别取决于nums1与nums2的有效长度。
二、链表相关OJ题
1 移除链表元素
leetcode 203.移除链表元素
题目解析
题目大意:和顺序表OJ题移除元素类似,但这次换成了链表形式。我们要找出链表中所有数据元素等于val的结点并删除,最后返回新链表的头节点。注意这里的链表是不带头单向不循环链表。
代码示例如下:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
typedef struct ListNode LTNode;//为了方便重命名简化一下
LTNode* removeElements(LTNode* head, int val)
{
LTNode* newhead, *ptail;//newhead为需要返回的新链表的头节点,ptail用于遍历链接新链表
newhead = ptail = NULL;
LTNode* pcur = head;//pcur用于遍历判断链表结点值
while (pcur)
{
if (pcur->val != val)//找到非val值结点,则链接新链表
{//链接前先判断新头节点newhead是否为空
if (!newhead)
{//newhead == NULL,赋值
ptail = newhead = pcur;
}
else
{//newhead != NULL,更新ptail的next指向
ptail->next = pcur;
ptail = ptail->next;
}
}
pcur = pcur->next;
}
if (ptail)
ptail->next = NULL;
return newhead;
}
虽然链表结点的删除操作相比顺序表简单得多,但这里面仍有许多细节需要注意。这里我们采取的思路是多个指针遍历链表同时记录与判断当前结点是否符合条件。
例如指针pcur
与ptail
,后者用于记录新链表的尾结点;前者用于判断当前结点是否符合条件,如果符合条件,我们需要找到新链表当前的尾结点并将符合条件的新结点链接到此尾结点之后。当然这里还需要判断一下新链表是否为空,也就是对newhead
判空,如果newhead
为空则ptail
也一定为空,所以需要对newhead
与ptail
进行赋值操作,将pcur
找到的第一个符合条件的结点赋值给newhead
与ptail
。
以此类推找下去,当ptail
找到最后一个符合条件的结点时,我们还需要注意链表可能并未结束,因为如果后面存在若干个数据元素等于val的结点,那么pcur
虽然会继续遍历跳过,直至为空结束循环,但ptail
却不会继续遍历,ptail
最后的终点一定是最后一个符合条件的结点,而最后一个符合条件的结点的next指针指向是不确定的,可能为空,也可能还跟着其他结点。所以需要增加一个ptail->next = NULL
的语句确保ptail
后面没有其他结点,同时为了防止ptail
为空,所以增加一个判断语句对ptail
判空。
2 反转链表
leetcode 206.反转链表
题目解析
题目大意:反转逆置任意单向不带头不循环链表,并返回新链表的新头结点,即原链表的尾结点。
代码示例如下:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
typedef struct ListNode LTNode;
struct ListNode* reverseList(struct ListNode* head)
{
if (!head)
{//链表为空直接返回
return head;
}
LTNode *n1 = NULL, *n2 = head, *n3 = n2->next;//n1,n2,n3分别记录前一结点,当前结点,后一结点
while (n2)
{
n2->next = n1;
n1 = n2;
n2 = n3;
if (n3)
n3 = n3->next;
}
return n1;
}
这里采取的思路同样是多个指针,同时记录了当前结点,前一结点,后一结点三个结点的位置,借此我们就可以遍历链表更改结点指针的指向了。
图示如下:
需要注意的是每一次循环的衔接与循环的结束条件。首先n2
不能为空,其次下一次循环前,需要按照n1->n2->n3
的顺序更新位置信息,先n1 = n2
,再n2 = n3
,接着确保n3
不为空的同时n3 = n3->next
。最后循环结束的条件是n2
为空,当n3
为空时n3
没有下一个结点,而n2 = n3 = NULL
,此时n1
成为新链表的头结点,也即原链表的尾结点,而n2
、n3
都为空。
3 链表的中间结点
leetcode 876.链表的中间结点
题目解析
题目大意:寻找链表的中间结点,若链表结点个数为偶数,则返回最中间的两个结点中的后一个结点,需要注意的是链表结点个数范围为[1, 100],即链表一定不为空。
代码示例如下:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
typedef struct ListNode LTNode;
struct ListNode* middleNode(struct ListNode* head)
{
LTNode* slow, *fast;
slow = fast = head;
while (fast && fast->next)
{
slow = slow->next;
fast = fast->next->next;
}
return slow;
}
示例采用的是快慢指针的思路,在初高中学习的时候我们一定遇见过”追及问题“。当两个人的速度为2倍关系时,两人走过的路程也一定为2倍关系。我们直接将其化为快慢指针法,fast
快指针走两步,slow
慢指针就走一步,这样它们的路程差为1倍,速度差也为1倍。
图示如下:
当fast
或fast->next
走至空就结束,slow
即为所需结点。
4 合并两个有序链表
leetcode 21.合并两个有序链表
题目解析
题目大意:合并两个升序链表,链表结点的范围是[0, 50],即两链表都可能为空。
代码示例如下:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
typedef struct ListNode ListNode;
ListNode* mergeTwoLists(ListNode* list1, ListNode* list2)
{
//其中一个链表为空,则直接返回另一个链表头节点即可
if (!list1)
{
return list2;
}
else if (!list2)
{
return list1;
}
//两链表都不为空
ListNode* l1, *l2;//用于遍历链表进行比较
l1 = list1;
l2 = list2;
ListNode* phead, *ptail;//phead为哨兵位记录新链表的头结点,ptail记录新链表的当前尾结点
phead = ptail = (ListNode*)malloc(sizeof(ListNode));//申请哨兵位结点
phead->next = ptail->next = NULL;
while (l1 && l2)//同时遍历两链表
{//比较判断l1,l2中结点的值,小的放到新链表尾结点后
if (l1->val < l2->val)
{//l1小,l1结点放到新链表的尾结点后,同时找到l1下一结点
ptail->next = l1;
ptail = ptail->next;
l1 = l1->next;
}
else
{//l2小或相等,l2结点放到新链表的尾结点后,同时找到l2下一结点
ptail->next = l2;
ptail = ptail->next;
l2 = l2->next;
}
}
//两链表长度不一,循环比较完后,可能有一链表还有剩余结点
if (l1)
{//l1剩余结点
ptail->next = l1;
}
if (l2)
{//l2剩余结点
ptail->next = l2;
}
ListNode* ret = phead->next;//返回值为哨兵位下一结点
free(phead);
phead = NULL;
return ret;
}
与顺序表的合并相同,示例采取的思路是同时遍历两链表,并进行比较判断,哪个小就取哪个结点放到新链表,最后判断一下两链表是否有剩余结点,将剩余结点放到新链表后。
5 链表的回文结构
牛客 OR36 链表的回文结构
题目解析
题目大意:判断链表是否为回文结构,所谓回文结构类似轴对称,即将一个链表从中间分开,前一段和后一段的逆置是相等的。例如1->2->3->2->1
可以从中间分为12
与21
,逆置任意一段则得到另外一段。
代码示例如下:
/*
struct ListNode {
int val;
struct ListNode *next;
ListNode(int x) : val(x), next(NULL) {}
};*/
typedef struct ListNode ListNode;
struct ListNode* middleNode(struct ListNode* head)
{
ListNode* slow, * fast;
slow = fast = head;
while (fast && fast->next)
{
slow = slow->next;
fast = fast->next->next;
}
return slow;
}
struct ListNode* reverseList(struct ListNode* head)
{
if (!head)
{
return head;
}
ListNode* n1 = nullptr, * n2 = head, * n3 = n2->next;
while (n2)
{
n2->next = n1;
n1 = n2;
n2 = n3;
if (n3)
n3 = n3->next;
}
return n1;
}
bool chkPalindrome(ListNode* A)
{
ListNode* midNode = middleNode(A);
ListNode* cur = A;
midNode = reverseList(midNode);
while (cur && midNode)
{
if (cur->val != midNode->val)
{
return false;
}
cur = cur->next;
midNode = midNode->next;
}
return true;
}
前面已经讲解过链表的中间结点以及链表的逆置等算法,这里可以直接拿来使用。思路大致为先得到中间结点,再逆置后一段链表结点,然后我们就可以分别遍历两段链表进行比较,若都对应相等则返回true,若不等则返回false。
图示如下:
6 相交链表
leetcode 160.相交链表
题目解析
题目大意:两个一定不为空的单链表,请判断它们是否相交,若相交则返回第一个相交结点,若不相交则返回NULL,注意返回时链表保存原始结构。
代码示例如下:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
typedef struct ListNode ListNode;
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB)
{
//计算两链表各自的长度
ListNode *curA = headA, *curB = headB;
int lenA = 1, lenB = 1;
while (curA->next != NULL)
{
lenA++;
curA = curA->next;
}
while (curB->next != NULL)
{
lenB++;
curB = curB->next;
}
//判断两链表的尾结点是否相交,相交链表的尾结点也一定相交
if (curA != curB)
{
return NULL;
}
int sub = abs(lenA - lenB);//计算两链表长度的差值
ListNode *LongList = headA, *ShortList = headB;
if (lenA < lenB)
{
LongList = headB;
ShortList = headA;
}
while (sub--)//长链表指针向后走若干步,使两链表处于相同长度的起始位置
{
LongList = LongList->next;
}
while (LongList && ShortList)//从相同长度的起始位置开始同时遍历两链表
{
if (LongList == ShortList)//判断两链表结点的地址是否相同
{//链表结点的地址相同,则返回当前结点
return LongList;
}
LongList = LongList->next;
ShortList = ShortList->next;
}
return NULL;//不相交返回空指针
}
示例采取的思路是将两链表变成等长,这样同时遍历两链表时,两个链表上的指针就一定可以同时到达相交结点了,若遍历完链表都没有找到相同的结点地址,则链表一定不相交。
7 环形链表的判断
leetcode 141.环形链表
题目解析
题目大意:判断链表是否有环,即链表的尾结点的next指针指向的是空,还是某个结点。需要注意链表可以为空。
代码示例如下:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
bool hasCycle(struct ListNode *head)
{
struct ListNode* slow = head, *fast = head;
while (fast && fast->next)
{
slow = slow->next;
fast = fast->next->next;
if (slow == fast) return true;
}
return false;
}
这里同样采取快慢指针即可,因为如果存在环就一定存在循环,使用快慢指针且速度差为1倍时,快慢指针一定会相遇。
8 环形链表
leetcode 142.环形链表 II
题目解析
题目大意:在判断链表是否存在环的基础上,需要我们将链表入环的第一个结点返回。
思路一:
首先我们先来看一段证明,
L为进环前的长度,X为进环后慢指针走的路程,R为环长。当快慢指针从头节点开始向后走时,慢指针走过L / 2时快指针走过L到达入环结点,当慢指针走过L时快指针在圈内走了L/2,由于环长与进环前的长度没有特定关系,所以不能确定快指针在圈内走过了多少圈。慢指针入环走了x步与快指针相遇在M点,此时一定是慢指针在环内的第一圈,因为慢指针走完一圈,快指针可以走过两圈,所以在第二圈之前两指针一定相遇了,同时由于速度差以及追击问题我们也可以确定快指针在相遇前一定已经走过一次M点,即快指针起码绕环转了一圈。
由相遇的整个过程分析可知,
快指针路程:L + X + nR (n为圈数,n >= 1)
慢指针路程:L + X
由速度关系得出: 2(L + X) = L + X + nR,即L = nR - X
我们得出L = nR - X的关系式要怎么用呢?我们先来变个形式。
变成:L = (n - 1)R + R - X。
其中(n - 1)R代表圈数乘以环长,而R - X代表了相遇结点重新走回入环点的距离。这说明我们只要用一个指针从头节点开始走L步,同时让相遇结点处的指针同样走L步,那么相遇结点的指针就会走(n - 1)圈最后走R-X步回到入环点,而这时从头节点开始走的指针也会走到入环点,我们只需要进行一个判断即可得到入环结点。
代码示例如下:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
typedef struct ListNode ListNode;
ListNode *detectCycle(ListNode *head)
{
//先判断是否存在环
ListNode* slow = head, *fast = head;
while (fast && fast->next)
{
slow = slow->next;
fast = fast->next->next;
if (slow == fast)
{
//环存在,让一个指针从头结点开始重新走,让一个指针从相遇结点继续在环内走
ListNode* meet = slow;
while (meet != head)
{
meet = meet->next;
head = head->next;
}
return meet;
}
}
return NULL;
}
思路二:
其实我们可以试着在相遇结点处将环断开,如下
先记录下meet->next
,用newhead
保存,同时将meet->next
置为空,这样链表就从meet
处断开了,就构成了两个链表,一个从a到b,一个从c到b,我们只需要将两个链表的头结点传给前面相交链表的函数中即可得出第一个相交结点,也即入环点。
代码示例如下:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
typedef struct ListNode ListNode;
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB)
{
//求相交链表的交点并返回
struct ListNode *curA = headA, *curB = headB;
int lenA = 1, lenB = 1;
while (curA->next != NULL)
{
lenA++;
curA = curA->next;
}
while (curB->next != NULL)
{
lenB++;
curB = curB->next;
}
if (curA != curB)
{
return NULL;
}
int sub = abs(lenA - lenB);
ListNode *LongList = headA, *ShortList = headB;
if (lenA < lenB)
{
LongList = headB;
ShortList = headA;
}
while (sub--)
{
LongList = LongList->next;
}
while (LongList && ShortList)
{
if (LongList == ShortList)
{
return LongList;
}
LongList = LongList->next;
ShortList = ShortList->next;
}
return NULL;
}
ListNode *detectCycle(ListNode *head)
{
ListNode* slow = head, *fast = head;
while (fast && fast->next)
{
slow = slow->next;
fast = fast->next->next;
if (slow == fast)
{
//断环,改为求相交链表问题
struct ListNode* meet = slow;
struct ListNode* newhead = meet->next;
meet->next = NULL;
meet = getIntersectionNode(head, newhead);
slow->next = newhead;
return meet;
}
}
return NULL;
}