【数据结构】强化训练:从基础到入门到进阶(2)
1 链表分割
题目解析:
不能改变原来的数据顺序”,更准确地说,是不能改变原来节点值的相对顺序。
结合这道链表题来理解:
假设原链表中,小于 x
的节点值依次是 a₁, a₂, ..., aₙ
,它们在原链表中的出现顺序是 a₁
先于 a₂
,a₂
先于 a₃
,以此类推;
大于等于 x
的节点值依次是 b₁, b₂, ..., bₘ
,它们在原链表中的出现顺序是 b₁
先于 b₂
,b₂
先于 b₃
,以此类推。
经过操作后,所有小于 x
的节点要排在所有大于等于 x
的节点之前,且在 “小于 x
的节点区域内”,a₁, a₂, ..., aₙ
的顺序必须和原链表中一致;在 “大于等于 x
的节点区域内”,b₁, b₂, ..., bₘ
的顺序也必须和原链表中一致。
思路:创建两个链表(小链表,大链表),遍历原链表,小的尾插到小链表中,大的尾插到大链表中,大链表和小链表首尾相连
ListNode* partition(ListNode* pHead, int x) {// write code hereListNode* lesshead,*lesstail;ListNode* greaterhead,*greatertail;lesshead=lesstail=(ListNode*)malloc(sizeof(ListNode));greaterhead=greatertail=(ListNode*)malloc(sizeof(ListNode));ListNode*pcur=pHead;while(pcur){if(pcur->val<x){lesstail->next=pcur;lesstail=lesstail->next;}else{greatertail->next=pcur;greatertail=greatertail->next;}pcur =pcur->next;}greatertail->next=NULL;lesstail->next=greaterhead->next;ListNode* prev=lesshead->next;free(lesshead);free(greaterhead);return prev;}
2 相交链表
思路:
(1)求两个链表的长度,求得两个链表的长度差为x
(2)让表较长的表头先走x步
(3)让两个指针一起走,看走到链表尾部前是否有两个链表指向相同的情况(找相同的节点)
typedef struct ListNode ListNode;
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {int la=0;int lb=0;ListNode* pcur=headA;ListNode* prev=headB;while(pcur){la++;pcur=pcur->next;} while(prev){lb++;prev=prev->next;}int gap=abs(la-lb);ListNode* longList=headA;ListNode* shortList=headB;if(la<lb){shortList=headA;longList=headB;}while(gap--){longList=longList->next;}while(shortList){if(shortList==longList){return shortList;}shortList=shortList->next;longList=longList->next;}return NULL;
}
3 环形链表
思路:快慢指针,慢指针每次走一步,快指针每次走两步。如果slow和fast指向同一节点,说明该链表带环。
在环形链表中,慢指针每次走一步,快指针每次走2,3,4,5........步,快慢指针在环形链表中一定会相遇
typedef struct ListNode ListNode;
bool hasCycle(struct ListNode *head) {ListNode* slow=head;ListNode* fast=head;while(fast&&fast->next){slow=slow->next;fast=fast->next->next;if(slow==fast)return true;}return false;
}
易错点:
1. 为什么循环条件是 while(fast && fast->next)
?
这是为了避免避免空指针访问错误,同时确保快指针能正常移动:
- 快指针每次要移动两步(
fast->next->next
),所以需要保证:fast
本身不为NULL
(否则无法访问fast->next
)fast->next
不为NULL
(否则无法访问fast->next->next
)
- 如果链表无环,快指针会先到达终点(遇到
NULL
),此时循环终止,返回false
。
2. 为什么要先移动指针,再判断 if(slow == fast)
?
这是由初始状态决定的:
- 初始时
slow
和fast
都指向head
(起点相同) - 如果先判断再移动,会直接误判空链表或单节点链表有环(因为初始状态下
slow == fast
)
4 环形链表Ⅱ
思路:快慢指针,在环里一定会相遇。
相遇点到入环节点的距离 == 头节点到入环节点的距离
typedef struct ListNode ListNode;
struct ListNode *detectCycle(struct ListNode *head) {ListNode* fast=head;ListNode* slow=head;while(fast&&fast->next){fast=fast->next->next;slow=slow->next;if(fast==slow){ListNode* pcur=head;while(pcur!=slow){pcur=pcur->next;slow=slow->next;}return pcur;}}return NULL;}
5 移动零
思路:双指针
使用双指针,左指针指向当前已经处理好的序列的尾部,右指针指向待处理序列的头部。
右指针不断向右移动,每次右指针指向非零数,则将左右指针对应的数交换,同时左指针右移。
注意到以下性质:
左指针左边均为非零数;
右指针左边直到左指针处均为零。
因此每次交换,都是将左指针的零与右指针的非零数交换,且非零数的相对顺序并未改变。
right
指针的作用是遍历整个数组的所有元素,因此在每一轮循环结束时,无论当前元素是 0 还是非零,right
都会执行right++
,确保能依次检查数组中的每个元素
举例说明(以 nums = [0,1,0,3,12]
为例):
- 初始:
left=0,right=0
(元素为 0)→ 不交换,right++
(right=1
)。 right=1
(元素为 1,非零)→ 交换后left++
(left=1
),right++
(right=2
)。right=2
(元素为 0)→ 不交换,right++
(right=3
)。right=3
(元素为 3,非零)→ 交换后left++
(left=2
),right++
(right=4
)。right=4
(元素为 12,非零)→ 交换后left++
(left=3
),right++
(right=5
,循环结束)。
最终数组变为 [1,3,12,0,0]
,符合预期。
代码实现:
void swap(int *a,int *b)
{int tmp=*a;*a=*b;*b=tmp;
}
void moveZeroes(int* nums, int numsSize) {int left=0,right=0;while(right<numsSize){if(nums[right]){swap(&nums[left],&nums[right]);left++;}right++;}
}
时间复杂度:O(n),其中 n 为序列长度。每个位置至多被遍历两次。
空间复杂度:O(1)。只需要常数的空间存放若干变量。
循环语句中的swap(&nums[left], &nums[right]),也可以表示为wap(nums + left, nums + right),
都是将数组中索引为 left
和 right
的两个元素进行交换。
样写的原因与 指针和数组的关系 有关:
- 在 C 语言中,数组名
nums
本质上是指向数组首元素的指针(nums
等价于&nums[0]
)。nums + left
表示指针向后偏移left
个位置,指向数组中索引为left
的元素(等价于&nums[left]
)。- 同理,
nums + right
等价于&nums[right]
,指向索引为right
的元素。而
swap
函数的参数要求是两个int*
类型的指针(即整数的地址),因此传递nums + left
和nums + right
正好符合函数的参数要求,实现了交换nums[left]
和nums[right]
的效果。
六 复写零
方法一:双指针
思路与算法
首先如果没有原地修改的限制,那么我们可以另开辟一个栈来进行模拟放置:
而实际上我们可以不需要开辟栈空间来模拟放置元素,我们只需要用两个指针来进行标记栈顶位置和现在需要放置的元素位置即可。我们用 top 来标记栈顶位置,用 i 来标记现在需要放置的元素位置,那么我们找到原数组中对应放置在最后位置的元素位置,然后在数组最后从该位置元素往前来进行模拟放置即可。
void duplicateZeros(int* arr, int arrSize){int top = 0;int i = -1;while (top < arrSize) {i++;if (arr[i] != 0) {top++;} else {top += 2;}}int j = arrSize - 1;if (top == arrSize + 1) {arr[j] = 0;j--;i--;} while (j >= 0) {arr[j] = arr[i];j--;if (!arr[i]) {arr[j] = arr[i];j--;} i--;}
}