算法学习打卡day3 |203.移除链表元素、707.设计链表、24. 两两交换链表中的节点、206.反转链表、92. 反转链表 II、25. K 个一组翻转链表

数组和链表的区别

  • 数组在内存空间上是连续的,而链表是通过指针域将各个节点连接,实现逻辑连续,而物理空间不联系
  • 数组在按索引查询时时间复杂度为O(1),而链表为O(n)
  • 数组在插入删除时,由于需要移动其他数组元素,故时间复杂度为O(n),而链表在插入删除的时间复杂度为O(1)。

今日学习题目:

203.移除链表元素

力扣题目链接
题目描述:
给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点 。

示例 1:

输入:head = [1,2,6,3,4,5,6], val = 6
输出:[1,2,3,4,5]
思路:

  • 遍历数组元素,因为链表删除元素需要找到前一个元素,所以用cur的下一个节点去判断val,找到后,原地删除即可。
  • 在链表中添加或删除节点(一般一点是在需要修改链表之间的指针指向时,要想到用虚拟头节点)时,尽量借助虚拟头节点,这样可以不用单独处理头节点的逻辑

代码实现

  1. 带虚拟头节点代码如下:
ListNode* removeElements(ListNode* head, int val) {
	if (head == nullptr) {
            return nullptr;
        }
        ListNode* virtual_head = new ListNode(0, head);
        ListNode* cur_head = virtual_head;
        while(cur_head->next) {
            if (cur_head->next->val == val) {
                ListNode* tmp_head = cur_head->next;
                cur_head->next = tmp_head->next;
                delete tmp_head;
            } else {
                cur_head = cur_head->next;
            }
            
        }
        head = virtual_head->next;
        delete virtual_head;
        return head;
    }
  1. 不带虚拟头节点代码如下:
	ListNode* removeElements(ListNode* head, int val) {
		if (head == nullptr) {
            return nullptr;
     	}
     	if (head && head->val == val) {
			ListNode* tmp_node = head;
			head = head->next;
			delete tmp_node;
		}
        ListNode* cur_head = head;
        while(cur_head && cur_head->next) {
            if (cur_head->next->val == val) {
                ListNode* tmp_head = cur_head->next;
                cur_head->next = tmp_head->next;
                delete tmp_head;
            } else {
                cur_head = cur_head->next;
            }
            
        }
        head = virtual_head->next;
        delete virtual_head;
        return head;
    }

通过对比发现使用虚拟头节点时不需要单独处理链表开始位置

707.设计链表

力扣题目链接
题目描述:
你可以选择使用单链表或者双链表,设计并实现自己的链表。

单链表中的节点应该具备两个属性:val 和 next 。val 是当前节点的值,next 是指向下一个节点的指针/引用。

如果是双向链表,则还需要属性 prev 以指示链表中的上一个节点。假设链表中的所有节点下标从 0 开始。

实现 MyLinkedList 类:

MyLinkedList() 初始化 MyLinkedList 对象。
int get(int index) 获取链表中下标为 index 的节点的值。如果下标无效,则返回 -1 。
void addAtHead(int val) 将一个值为 val 的节点插入到链表中第一个元素之前。在插入完成后,新节点会成为链表的第一个节点。
void addAtTail(int val) 将一个值为 val 的节点追加到链表中作为链表的最后一个元素。
void addAtIndex(int index, int val) 将一个值为 val 的节点插入到链表中下标为 index 的节点之前。如果 index 等于链表的长度,那么该节点会被追加到链表的末尾。如果 index 比长度更大,该节点将 不会插入 到链表中。
void deleteAtIndex(int index) 如果下标有效,则删除链表中下标为 index 的节点。

示例:

输入
[“MyLinkedList”, “addAtHead”, “addAtTail”, “addAtIndex”, “get”, “deleteAtIndex”, “get”]
[[], [1], [3], [1, 2], [1], [1], [1]]
输出
[null, null, null, null, 2, null, 3]

思路:

  • 使用虚拟头节点进行插入和删除

代码实现

class MyLinkedList {
public:
    struct LinkedNode{
        int val;
        LinkedNode *next;
        LinkedNode(int val) : val(val), next(nullptr) {}    
    };
    MyLinkedList() {
        _pri_node = new LinkedNode(0);
        _size = 0;
    }
    
    int get(int index) {
        if (index >= _size) {
            return -1;
        }
        LinkedNode* cur_node = _pri_node;
        while(index--) {
            cur_node = cur_node->next;
        }
        return cur_node->next->val;
    }
    
    void addAtHead(int val) {
       LinkedNode* new_node = new LinkedNode(val);
       LinkedNode* tmp_node = _pri_node->next;
       _pri_node->next = new_node;
       new_node->next = tmp_node;
       _size++;
    }
    
    void addAtTail(int val) {
        LinkedNode* new_node = new LinkedNode(val);
        LinkedNode* cur_node = _pri_node;
        while(cur_node->next) {
            cur_node = cur_node->next;
        }
        cur_node->next = new_node;
        
        //cout << cur_node->val << " " << _pri_node->next->val;
        _size++;
    }
    
    void addAtIndex(int index, int val) {
        if (index > _size) {
            return;
        }
        LinkedNode* new_node = new LinkedNode(val);
        LinkedNode* cur_node = _pri_node;
        while(index--) {
            cur_node = cur_node->next;
        }
        new_node->next = cur_node->next;
        cur_node->next = new_node;
        _size++;
    }
    
    void deleteAtIndex(int index) {
        if (index >= _size) {
            return;
        }
        LinkedNode* cur_node = _pri_node;
        while(index--){
            cur_node = cur_node->next;
        }
        LinkedNode* tmp_node = cur_node->next;
        cur_node->next = tmp_node->next;
        delete tmp_node;
        _size--;
    }
private:
    LinkedNode* _pri_node;
    int _size;
};

24. 两两交换链表中的节点

力扣题目链接
题目描述:
给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。
示例 1:

输入:head = [1,2,3,4]
输出:[2,1,4,3]
思路:

  • 这道题就是手动模拟的题目,比较容易混,首先,因为需要改变链表之间的指针指向,建立一个虚拟头节点,然后将交换时分四步,
    • 第一步: 用tmp和tmp1分别保存虚拟头节点的下一个节点(交换后的第二个节点)和第三个节点(防止断链)
    • 第二步: 将虚拟头节点指向第二个节点
    • 第三步: 将第二个节点指向第一个节点,因为虚拟节点的next改变,所以此时指向tmp
    • 第四步: 将第一个节点指向第三个节点,此时指向tmp1.
  • 这样就完成了前两个节点的交换,然后后面节点一次类推
    如图:
    算法学习打卡day3 |203.移除链表元素、707.设计链表、24. 两两交换链表中的节点、206.反转链表、92. 反转链表 II、25. K 个一组翻转链表_第1张图片

代码实现:

ListNode* swapPairs(ListNode* head) {
        ListNode* virtual_node = new ListNode(0, head);
        ListNode* cur = virtual_node;
        while(cur->next && cur->next->next) {
            //保存节点,防止断链
            ListNode* tmp = cur->next;
            ListNode* tmp1 = cur->next->next->next;

            //交换逻辑
            cur->next = cur->next->next; //cur指向第二个节点
            cur->next->next = tmp;            //第二个节点指向第一个节点
            cur->next->next->next = tmp1;   //第一个节点指向第三个节点
            cur = cur->next->next;      //当前节点移动到第三个节点前,用于下一轮交换
        }
        head = virtual_node->next;
        delete virtual_node;
        return head;
    }

206.反转链表

力扣题目链接
题目描述:
给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。

示例 1:

输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]

思路:

  • 两种方法,迭代法和递归法,递归法
  • 迭代法:定义三个指针,pre存放cur前一个指针,用于交换指针域,cur用于遍历链表,tmp存放cur下一个元素,用于防止断链,然后按图下步骤交换指针域,最后pre为头节点

代码实现:

//迭代法
ListNode* reverseList(ListNode* head) {
        if (head == nullptr) {
            return head;
        }
        ListNode* pre_node = nullptr;
        ListNode* cur_node = head;
        ListNode* tmp_node = nullptr;
        while(cur_node) {
            tmp_node = cur_node->next;
            cur_node->next = pre_node;
            pre_node = cur_node;
            cur_node = tmp_node;
        }
        return pre_node;
    }
  • 递归法:对于递归算法,最重要的就是明确递归函数的定义。具体来说,我们的reverseList函数定义是这样的:输入一个节点head,将「以head为起点」的链表反转,并返回反转之后的头结点。
    代码如下:
ListNode* reverseList(ListNode* head) {
        if (head == nullptr || head->next == nullptr) {
            return head;
        }
        ListNode* new_node = reverseList(head->next);//新的头节点
        head->next->next = head;
        head->next = nullptr;
        return new_node;
    }

ListNode* new_node = reverseList(head->next);
这一行代码得到的结果其实是下图这样的,看完图理解代码其实比较好理解了,new_node为我们反转之后的头节点
算法学习打卡day3 |203.移除链表元素、707.设计链表、24. 两两交换链表中的节点、206.反转链表、92. 反转链表 II、25. K 个一组翻转链表_第2张图片

总结:

  • 虽然递归法看起来简洁,但是它的空间复杂度为O(n),比迭代法O(1)更大。

92. 反转链表 II

力扣题目链接
题目描述:
给你单链表的头指针 head 和两个整数 left 和 right ,其中 left <= right 。请你反转从位置 left 到位置 right 的链表节点,返回 反转后的链表 。
示例 1:

输入:head = [1,2,3,4,5], left = 2, right = 4
输出:[1,4,3,2,5]

思路:

  • 这个题是反转链表的升级,首先是实现反转前N个链表的反转,然后将区间反转转化为前N个链表反转即可。
  • 如何实现前N个元素反转?
    • 其实稍微修改下反转链表递归函数即可,我们增加参数n,然后每递归一次就减1,递归函数的退出条件也是 n == 1 的时候,同时要记录successor为head->next,因为刚是反转整个链表所以为nullptr,而这次不一定是最后一个节点,所以我们的head->next 不再是null,而是我们退出时的后驱节点successor.
      代码如下:
ListNode* successor = nullptr;
ListNode* reverseListN(ListNode* head, int n) {
        if (n == 1) {
        	successor = head->next;
            return head;
        }
        ListNode* new_node = reverseList(head->next, n - 1);//新的头节点
        head->next->next = head;
        head->next = successor;
        return new_node;
    }
  • 如何转化为求前N个节点?
    • 思考:如果我们把head的索引视为 1,那么我们是想从第left个元素开始反转是吧?如果把head.next的索引视为 1 呢?那么相对于head.next,反转的区间应该是从第 left - 1个元素开始的;那么对于head.next.next呢……
    • 那么,将 left == 1作为递归退出的判断条件,然后不等于1时,我们就让下一个节点去递归调用函数,此时区间就减少1,直到left减为1就找到了我们要反转的地方,然后就可以调用反转前N个节点执行反转,而我们递归函数只需要把下一个节点反转的结果接到head后面即可。
    • 代码如下:
ListNode* reverseBetween(ListNode* head, int left, int right) {
        if (left == 1) {
            return reverseListN(head, right);
        }
        head->next = reverseBetween(head->next, left - 1, right - 1);
        return head;
    }

整体代码实现如下:

ListNode* successor = nullptr;
    ListNode* reverseListN(ListNode* head, int n) {
        if (n == 1) {
        	successor = head->next;
            return head;
        }
        ListNode* new_node = reverseListN(head->next, n - 1);//新的头节点
        head->next->next = head;
        head->next = successor;
        return new_node;
    }
    ListNode* reverseBetween(ListNode* head, int left, int right) {
        if (left == 1) {
            return reverseListN(head, right);
        }
        head->next = reverseBetween(head->next, left - 1, right - 1);
        return head;
    }

25. K 个一组翻转链表

力扣题目链接
题目描述:
给你链表的头节点 head ,每 k 个节点一组进行翻转,请你返回修改后的链表。

k 是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。

你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。

示例 1:

输入:head = [1,2,3,4,5], k = 2
输出:[2,1,4,3,5]

思路:

  • 本题是 24. 两两交换链表中的节点 升级,而本题是k个一组交换。
  • 还是递归法,分解为如下几步:
    1. 先反转以head开头的k个节点(反转的逻辑就是普通链表反转的迭代法,只不过普通链表反转是反转第一个节点到nullptr,而现在把nullptr改为了right)
    2. 将k + 1个元素开头的节点递归调用reverseKGroud函数
    3. 将第k个节点和k+1后面反转的节点连接即可
    4. 递归退出条件为,剩余节点小于k个

代码实现

ListNode* reverse(ListNode* left, ListNode* right) {
        ListNode* pre = nullptr, *cur = left;
        while(cur != right) {
            ListNode* tmp = cur->next;
            cur->next = pre;
            pre = cur;
            cur = tmp;
        }
        return pre;
    }
    ListNode* reverseKGroup(ListNode* head, int k) {
        ListNode* cur = head;
        int i = k;
        while(i--) {
            if (cur == nullptr) {
                return head;
            }
            cur = cur->next;
        }
        ListNode* new_node = reverse(head, cur);
        head->next = reverseKGroup(cur, k);

        return new_node;
    }

你可能感兴趣的:(算法学习打卡,算法,学习,链表)