数据结构算法100天

本文深入探讨链表的经典操作,包括使用虚拟头节点、快慢指针对链表的反转、指定区间反转、每k个一组反转、环的检测、两有序链表的合并以及删除倒数第n个节点的方法。通过递归、迭代等策略,详细讲解了这些操作的实现,并提供了相关LeetCode题目的解题思路。
摘要由CSDN通过智能技术生成

数据结构算法100天

Day01

链表经典操作

  • 虚拟头节点(涉及插入和删除操作时使用)
  • 快慢指针(涉及查找链表元素时使用)
  • 递归
  • 迭代

链表经典习题

//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;
}

#【题目】:将一个节点数为 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 个一组翻转,返回翻转后的链表
  如果链表中的节点数不是 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;
}
//快慢指针判断链表是否有环
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;
}
//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;
}

【分析】: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;
}
//返回链表的中间节点
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;
}

链表常见问题题解总结

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值