数据结构算法100天
Day01
链表经典操作
- 虚拟头节点(涉及插入和删除操作时使用)
- 快慢指针(涉及查找链表元素时使用)
- 递归
- 迭代
链表经典习题
- 单链表反转 leetcode_206–递归/迭代
//1.双指针迭代法
struct ListNode* reverseList(struct ListNode* head) {
//定义两个指针 pre 在前, curr在后
struct ListNode* curr = NULL;
struct ListNode* pre = head;
//每次让 pre的next指向curr,实现一次局部反转,直至pre为空
while (pre) {
//每次局部逆转之前,先保存pre的后一个节点的位置
struct ListNode* next = pre->next;
//局部逆转
pre->next = curr;
//局部逆转完成后,分别让 curr 和 pre 指针前移一位
curr = pre;
pre = next;
}
return pre;
}
//2.递归法
struct ListNode* reverseList(struct ListNode* head)
//递归终止条件,链表为空或者 当前指针已经移动至最后一个结点
if (head == NULL || head->next == NULL) {
//返回当前节点
return head;
}
//递归调用传入下一个节点,目的是为了到达最后一个节点
struct ListNode* newHead = reverseList(head->next);
//到达最后一个节点后,开始出栈;
//将当前节点的下一个节点的next指向当前节点
head->next->next = head;
//并将当前节点的next的next置空
head->next = NULL;
return newHead;
}
- 链表内指定区间反转 newCoder–BM2 链表指定区间反转
#【题目】:将一个节点数为 size 链表 m 位置到 n 位置之间的区间反转,要求时间复杂度 O(n)O(n),空间复杂度 O(1)O(1)。
#例如:
#给出的链表为 1 -> 2 -> 3 -> 4 -> 5 -> NULL, m=2,n=4
#返回 1-> 4-> 3-> 2-> 5-> NULL
//1. 虚拟结点 + 头插法
LNode* reverseBetween(LNode* head, int m, int n ) {
//创建哑节点,以便后面进行插入操作
LNode* dummy = (LNode*)malloc(sizeof(LNode));
dummy->val = 0;
dummy->next = head;
LNode* pre = dummy;
//工作指针
LNode* curr = head;
//temp指针用来在反转链表时保存工作指针的下一个位置
LNode* temp = NULL;
//先让工作指针 curr 走 m -1 步 ,走到 第 m 个节点处 ,pre 保持在 curr 的前一个位置
for(int i = 0; i < m - 1 ; i++) {
pre = curr;
curr = curr->next;
}
//此处的思想是
//不妨拿出四本书,摞成一摞(自上而下为 A B C D),要让这四本书的位置完全颠倒过来(即自上而下为 D C B A):
//盯住书A,每次操作把A下面的那本书放到最上面
//初始位置:自上而下为 A B C D
//第一次操作后:自上而下为 B A C D
//第二次操作后:自上而下为 C B A D
//第三次操作后:自上而下为 D C B A
//其实这里就是头插法思想(每次都把后一个元素插入到最前面),需要插 n - m 次
for(int i = 0 ; i < n - m ; i ++ ) {
//保存工作指针的后一个元素,工作指针指向节点的next指向下一个节点的next指向的节点,即前移一位
temp = curr->next;
curr->next = temp->next;
//把工作指针指向元素的后一个元素插入到pre的后面,即 被反转链表的最前面
temp->next = pre->next;
pre->next = temp;
}
return dummy->next;
}
public class Solution {
// 双指针(两次遍历)
//说明:方便理解,以下注释中将用left,right分别代替m,n节点
public ListNode reverseBetween (ListNode head, int m, int n) {
//设置虚拟头节点
ListNode dummyNode = new ListNode(-1);
dummyNode.next = head;
ListNode pre = dummyNode;
//1.走left-1步到left的前一个节点
for(int i=0;i<m-1;i++){
pre = pre.next;
}
//2.走roght-left+1步到right节点
ListNode rigthNode = pre;
for(int i=0;i<n-m+1;i++){
rigthNode = rigthNode.next;
}
//3.截取出一个子链表
ListNode leftNode = pre.next;
ListNode cur = rigthNode.next;
//4.切断链接
pre.next=null;
rigthNode.next=null;
//5.反转局部链表
reverseLinkedList(leftNode);
//6.接回原来的链表
pre.next = rigthNode;
leftNode.next = cur;
return dummyNode.next;
}
//反转局部链表
private void reverseLinkedList(ListNode head){
ListNode pre = null;
ListNode cur = head;
while(cur!=null){
//Cur_next 指向cur节点的下一个节点
ListNode Cur_next = cur.next;
cur.next = pre;
pre = cur;
cur = Cur_next ;
}
}
}
- **链表中的节点每 k 个 一组进行反转 ** newCoder–BM3 链表中的节点每 k 个 一组进行反转
【描述】:
将给出的链表中的节点每 k 个一组翻转,返回翻转后的链表
如果链表中的节点数不是 k 的倍数,将最后剩下的节点保持原样
你不能更改节点中的值,只能更改节点本身。
【例如】:
给定的链表是 1->2->3->4->5
对于 k = 2 , 你应该返回 2-> 1-> 4-> 3-> 5
对于 k = 3 , 你应该返回 3->2 ->1 -> 4-> 5
//此函数反转区间是 [left,right) ,通过迭代反转的方式实现
struct ListNode* reverse(struct ListNode* left ,struct ListNode* right) {
struct ListNode* pre = right;
struct ListNode* next = NULL;
while(left != right) {
next = left->next;
left->next = pre;
pre = left;
left = next;
}
return pre;
}
//链表中的节点每 k 个 一组进行反转
struct ListNode* reverseKGroup(struct ListNode* head, int k ) {
// 定义工作指针指向 head
struct ListNode* curr = head;
//遍历出每一个长度为 k+1 的链表,反转前 k 个节点
for(int i = 0 ; i < k ; i ++ ) {
//如果在遍历过程中,发现 curr 走到了 NULL,说明链表长度不足,不反转
//直接返回此时的头节点
if(curr == NULL) {
return head;
}
curr = curr->next;
}
//递归调用reverse, 反转前 k 个节点
struct ListNode* res = reverse(head,curr);
//把每一个反转后的链表依次从后连接起来
head->next = reverseKGroup(curr,k);
//返回的是第 k + 1个节点
return res;
}
- 链表中环的检测 leetcode_141–快慢指针
//快慢指针判断链表是否有环
bool hasCycle(LNode* head) {
//【思路】
//设计两个快慢指针,两指针从头节点出发开始移动;
//快指针每次移动两步,慢指针每次移动一步;
//链表没有环时,快指针一直在慢指针前面,此时当快指针移动到最后一个元素时,返回false;
//如果链表中有环,快指针将会在环中追上慢指针,此时返回true,表示存在环.
LNode* fast = head;
LNode* slow = head;
//如果快指针不为空(空链表情况)以及快指针的next(一个结点和快指针移动到最后一个结点时)不为空时循环
while(fast != NULL && fast->next != NULL) {
//快指针移动两步
fast = fast->next->next;
//慢指针移动一步
slow = slow->next;
//快指针追上慢指针时,表示存在环
if (slow == fast) {
return true;
}
}
//循环结束时(快指针移动到最后一个元素)没有发现有环,返回false
return false;
}
- 两个有序的链表合并 leetcode_21—递归/迭代
//1.递归
LNode* mergeTwoLists(LNode* l1, LNode* l2) {
//递归边界
if(l1 == NULL) {
return l2;
}
if(l2 == NULL) {
return l1;
}
//子问题: mergeTwoLists(l1,l2)等价于 l1->next = mergeTwoLists(l1->next,l2);
//如果l1对应的结点值小于等于l2对应的结点值,最终合并的链表头节点一定为最底层栈中的l1
if(l1->val <= l2->val) {
//当前层需要将以l1后一个结点为头节点的链表与l2合并,返回的链表连到l1->next后面
l1->next = mergeTwoLists(l1->next,l2);
//递归工作栈全部结束后,最底层栈中l1指向的链表即最终合并的链表
return l1;
}
//如果l2对应的结点值小于l1对应的结点值
l2->next = mergeTwoLists(l1,l2->next);
return l2;
}
//2.迭代【使用虚拟(哑)结点】
LNode* mergeTwoLIsts(LNode* l1, LNode* l2) {
//定义一个虚拟结点(初始时其 next 指向l1)
LNode* prehead = (LNode*)malloc(sizeof(LNode));
prehead->next = l1;
//定义一个初始时指向虚拟结点的指针
LNode* prev = prehead;
//当其中一个链表已经被合并完时,跳出循环
while(l1 != NULL && l2 != NULL) {
//如果l1的值小于等于l2的值,将prev的next指向l1,并将l1向后移动
//尾插法
if(l1->val <= l2->val) {
prev->next = l1;
l1 = l1->next;
}else {
prev->next = l2;
l2 = l2->next;
}
//每趟循环更新prev指针(prev指针后移一位)
prev = prev->next;
}
//合并后 l1 和 l2 最多只有一个还未被合并完,我们直接将链表末尾指向未合并完的链表即可
prev->next = (l1 == NULL) ? l2 : l1;
//最后返回以虚拟结点下一个节点为头节点链表
return prehead->next;
}
- 删除链表倒数第 n 个结点 leetcode_19–双指针+哑结点
【分析】:first指针先移动 n 个位置, 假设链表长 length , 此时链表的表尾 NULL 距离 first 指针为 (length + 1 )- (n + 1),即为 length - n 个距离 , first指针继续向后移动至 NULL,需要移动 length - n 步,此时工作指针也从 head 向后移动 length - n 步,移动完毕后,工作指针距离链表的最后一个节点为 length - (length-n + 1),即为 n -1 个距离,所以此时工作指针所指节点为 倒数第 n 个节点
//删除单链表倒数第k个节点
//1.双指针 + 哑节点
LNode* removeNthFromEnd(LNode* head, int n) {
//定义两个指针(双针)
//【思路】
//先让first指针移动到第n个位置,再让preDelte指针开始从头移动,
//同时first指针继续向后移动,直至first指针移动到最后的NULL
//删除节点时,需要得到该节点的前一个节点,所以这里采用虚拟结点,
//并使虚拟结点的 next 指向head
LNode* first = head;
//创建虚拟结点并初始化,next指向 head
LNode* dummy = (LNode*) malloc(sizeof(LNode));
dummy->val = -1;
dummy->next = head;
//将 preDelte 指向 dummy ,此后使用 preDelte 进行移动
LNode* preDelte = dummy;
//first先移动 n 个位置
for (int i = 0; i < n; i++)
{
first = first->next;
}
//first继续移动,直至first移动到NULL,preDelte同时与其移动相同的步数
//此时 preDelte刚好移动至被删除
while(first != NULL) {
first = first->next;
preDelte = preDelte->next;
}
//循环结束后preDelte结点即为目标结点的前驱结点
preDelte->next = preDelte->next->next;
//防止头节点被删除
LNode* ans = dummy->next;
//释放dummy 的内存空间
free(dummy);
//返回最终的头节点
return ans;
}
//2.两次遍历,第一次遍历求出链表长度 length ,第二次遍历求出所要删除位置(倒数第 n 个节点)
// 即 length - n + 1 (下标从1开始)
// 删除节点还是需要找到 目标结点的前驱节点便于删除
int getLength(struct ListNode* head) {
int length = 0;
while (head) {
++length;
head = head->next;
}
return length;
}
struct ListNode* removeNthFromEnd(struct ListNode* head, int n) {
//创建 虚拟结点(哑巴节点)
struct ListNode* dummy = malloc(sizeof(struct ListNode));
//初始化 哑节点
dummy->val = 0, dummy->next = head;
//获取链表长度
int length = getLength(head);
//工作指针指向初始时指向 哑节点
struct ListNode* cur = dummy;
//根据长度,遍历到目标结点的前驱节点
for (int i = 1; i < length - n + 1; ++i) {
cur = cur->next;
}
//删除目标结点
cur->next = cur->next->next;
//防止目标结点为头节点,所以不能 直接返回 head, 因为 head 可能已经被删除了
//创建指针 指向 dummy的下一个节点
struct ListNode* ans = dummy->next;
//释放 dummy 节点内存空间
free(dummy);
//返回最终的头节点
return ans;
}
//3.使用栈:弹出栈的第 n 个节点即为倒数 第 n 个节点 (先进后出)
struct Stack {
struct ListNode* val;
struct Stack* next;
};
struct ListNode* removeNthFromEnd(struct ListNode* head, int n) {
struct ListNode* dummy = malloc(sizeof(struct ListNode));
dummy->val = 0, dummy->next = head;
struct Stack* stk = NULL;
struct ListNode* cur = dummy;
while (cur) {
struct Stack* tmp = malloc(sizeof(struct Stack));
tmp->val = cur, tmp->next = stk;
stk = tmp;
cur = cur->next;
}
for (int i = 0; i < n; ++i) {
struct Stack* tmp = stk->next;
free(stk);
stk = tmp;
}
struct ListNode* prev = stk->val;
prev->next = prev->next->next;
struct ListNode* ans = dummy->next;
free(dummy);
return ans;
}
- 求链表的中间结点 leetcode_876–快慢指针
//返回链表的中间节点
LNode* middleNode(LNode* head){
if(head == NULL) {
return NULL;
}
//定义快慢指针,fast每次移动两步,slow 每次移动一步
//当链表中一共有 奇数 个节点时 ,fast 移动到链表最后一个节点时停止,同时 slow 也移动相同次数
//当链表中一共有 偶数 个节点时 ,fast 移动到链表表尾的 NULL 时停止,同时 slow 也移动相同次数
//slow 指针指向的位置即为中间节点
LNode* fast = head;
LNode* slow = head;
//如果 fast不为空(偶数长度时),并且 fast的next也不为空(奇数长度时)
while(fast != NULL && fast->next != NULL) {
fast = fast->next->next;
slow = slow->next;
}
return slow;
}