代码随想录Day 3 | 链表Part 1


Day 1 习题

二分法

34. 在排序数组中查找元素的第一个和最后一个位置

本题进一步练习二分法区间收窄时的细节控制,及分别使用两个函数(或一个函数两个判断条件)寻找左右位置的整体思路。

class Solution {
public:
    int binarySearchLeft(vector<int>& nums, int target){
        // 搜索第一个和目标值相等的元素的位置,如果没有,则会返回第一个比目标值大的元素的位置
        int left = 0;
        int right = nums.size();
        int leftBoundary = -2;
        while (left < right){
            int middle = left + (right - left) / 2;
            if (nums[middle] >= target){
                right = middle;
                leftBoundary = right;
            }
            else
                left = middle + 1;
        }
        return leftBoundary;
    }
    int binarySearchRight(vector<int>& nums, int target){
        // 搜索第一个比目标值大的元素的位置
        int left = 0;
        int right = nums.size();
        int rightBoundary = -2;
        while (left < right){
            int middle = left + (right - left) / 2;
            if (nums[middle] > target)
                right = middle;
            else{
                left = middle + 1;
                rightBoundary = left;
            } 
        }
        return rightBoundary;
    }
    
    vector<int> searchRange(vector<int>& nums, int target) {
        int leftBoundary = binarySearchLeft(nums, target);
        int rightBoundary = binarySearchRight(nums, target);
        if (leftBoundary == -2 || rightBoundary == -2)
            return {-1, -1};
        else if (leftBoundary == rightBoundary)
            return {-1, -1};
        else
            return {leftBoundary, rightBoundary - 1};
        
    }
};

双指针

283. 移动零

比较经典的一道题目,之前做过。

class Solution {
public:
    void moveZeroes(vector<int>& nums) {
        int slow = 0;
        for (int fast = 0; fast < nums.size(); fast++){
            if (nums[fast] == 0){
                continue;
            }
            int temp = nums[fast];
            nums[fast] = nums[slow];
            nums[slow] = temp;
            slow++;
        }
    }
};

Day 2 习题

滑动窗口

904. 水果成篮

思路类似标准题。

class Solution {
public:
    int totalFruit(vector<int>& fruits) {
        if (fruits.size() == 1)
            return 1;
        int maxLength = 0;
        int currentLength = 1;
        int left = 0;
        int right = 1;
        int firstType = fruits[0];
        //找到第二种果子的起始位置并记录
        while (right < fruits.size() && fruits[right] == firstType){
            right++;
            currentLength++;
        }
        if (right == fruits.size())
            return currentLength;
        int secondType = fruits[right];
        for (; right < fruits.size(); right++){
            //符合两种果子的要求时,不断增加长度
            if (fruits[right] == firstType || fruits[right] == secondType){
                currentLength += 1;
                continue;
            }
            //遇到第三种果子,记录本次采摘总数,并和最长历史纪录作比较
            maxLength = max(currentLength, maxLength);
            //重置总长度,更新果子种类,并由后向前地对新第一种果子的数目进行统计,以作为新长度的一部分
            int newLeft = right - 1;
            firstType = fruits[newLeft];
            secondType = fruits[right]; 
            currentLength = 1;
            while(fruits[newLeft--] == firstType)
                currentLength += 1;   
        }
        return max(maxLength, currentLength);
    }
};

Day 3 链表Part 1

链表理论基础

注意释放内存:

ListNode* tmp = cur->next;
cur->next = cur->next->next;
delete tmp;

203. 移除链表元素

不使用虚拟头节点时,头指针需要和后面的循环分开判断。
使用虚拟头节点:

class Solution {
public:
    ListNode* removeElements(ListNode* head, int val) {
        ListNode* dummyHead = new ListNode(0, head);

        ListNode* cur = dummyHead;
        while (cur->next != NULL)
        {
            if (cur->next->val == val)
            {
                ListNode* tmp = cur->next;   
                cur->next = cur->next->next;
                delete tmp;
            }
            else
                cur = cur->next;
        }
        //注意删除虚拟头
        head = dummyHead->next;
        delete dummyHead;
        return head;
    }

707. 设计链表

照着网站上的答案敲了一遍,涉及到index与size判断的地方,有时思路不够清晰,容易马虎。对于一个链表结构所包含的节点结构体、哑头、链表大小及常规操作需要再熟悉。


206. 反转链表

首先比较简单直接的想法是,想要反转某一个节点,则必须同时记录其前面一个节点和后面一个节点,算上它自己,共三个节点。对于链表长度不足三个节点的情况,分情况讨论:

class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        ListNode* pre;
        ListNode* cur;
        ListNode* nxt;
        //在反转时,一定同时需要储存三个节点。首先考虑节点数小于三个的情况
        if (head == NULL)
            return NULL;
        if (head->next == NULL)
            return head;
        if (head->next->next == NULL){
            cur = head->next;
            cur->next = head;
            head->next = NULL;
            return cur;
        }
        //节点数大于三个时,逐步遍历,停止条件为下一个端点为空。
        pre = head;
        cur = head->next;
        nxt = cur->next;
        head->next = NULL;
        while (nxt){
            cur->next = pre;
            pre = cur;
            cur = nxt;
            nxt = nxt->next;
        }
        //当下一个端点为空时退出循环。此时最后一个节点仍未反转,因此要单独处理一步。
        cur->next = pre;
        return cur;
    }
};

上面的方法显然很不简练,下面考虑对不同情况进行归并。

  1. 当整个链表只有两个节点时,下一个节点为空,这种情况就是循环退出时的情况,所以并不需要对这种情况单独讨论,可以直接删掉。
  2. 链表为空或只有一个节点的情况,都可以直接返回head,因此前两个if判断可以合并为一个;
  3. 在最开始定义三个节点时就可以直接给它们赋值。

整理后的代码如下:

class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        if (head == NULL || head->next == NULL)
            return head;
        ListNode* pre = head;
        ListNode* cur = head->next;
        ListNode* nxt = cur->next;
        head->next = NULL;
        while (nxt){
            cur->next = pre;
            pre = cur;
            cur = nxt;
            nxt = nxt->next;
        }
        cur->next = pre;
        return cur;
    }
};

代码简洁了不少,但是循环结束还要额外进行一步处理,不太漂亮。考虑循环的终止条件,造成最后要额外处理一步的原因是:当指向下一个节点的指针为NULL时,就需要停止,否则下一次循环再寻找节点时NULL->next无意义会报错,而这时最后一个节点还没处理。
因此考虑怎么让最后一个当前节点先处理完再做终止判断。此时想到:循环终止条件可以为当前节点而不是下一个节点,换句话说,不是将三个节点同时向后移,而是先移动当前节点,等确认了当前节点存在后,再访问其下一个节点
按照这个思路的代码修改如下,可以发现此时nxt节点的定义并没有一开始给出,而是移到了循环判断中。另外,发现单节点链表的情况同样可以归并到循环内。

class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        if (head == NULL)
            return head;
        ListNode* pre = head;
        ListNode* cur = head->next;
        ListNode* nxt;
        head->next = NULL;
        while (cur){
            nxt = cur->next;
            cur->next = pre;
            pre = cur;
            cur = nxt;
        }
        return pre;
    }
};

反思:为什么一开始没有想到用cur做循环判断而是用了nxt呢?原因是当我最开始想到必须用三个变量来储存节点时,我就把它们三个的地位平等地看待,脑海中的画面也一直是三个指针向后推进直到链表尾部,自然就会想到用nxt节点做终止判断。而实际上nxt指针只是一个附属性的存在,它只负责存储节点,而并不进行实际的操作。这也是为什么标准答案中将其取名为tmp。进一步地,同样prev也只负责存储节点而不进行实际操作,为什么给人感觉它的地位比nxt高?是因为在链表开头下意识地让它指向了head,cur没办法作为一个独立的指针对每一个节点都做处理。因此考虑让cur直接指在head的位置,prev置于NULL,这样连最开始的判断都省去了,得到最终版本:

class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        ListNode* pre = NULL;
        ListNode* cur = head;
        ListNode* nxt;
        while (cur){
            nxt = cur->next;
            cur->next = pre;
            pre = cur;
            cur = nxt;
        }
        return pre;
    }
};

反思:将当前节点作为中心,让它遍历整个链表的每一个节点,以这种思路去切入,可能会得到更通用和精简的解答。


C++语法

数组用花括号表示,如 {-1, 2, 3}。


总结

还没上道儿。

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值