代码随想录拓展day3 922. 按奇偶排序数组II;24. 两两交换链表中的节点;234.回文链表;35.搜索插入位置

代码随想录拓展day3 922. 按奇偶排序数组II;24. 两两交换链表中的节点;234.回文链表;35.搜索插入位置

数组和链表的题目。链表的操作几天没看又忘了,果然是要及时复习加反复复习。

922. 按奇偶排序数组II

922. 按奇偶排序数组 II - 力扣(Leetcode)

关键点是一半整数是 奇数 ,一半整数是 偶数,这个一半很重要,不要想复杂了。

思路

方法一

其实这道题可以用很朴实的方法,时间复杂度就就是O(n)了,C++代码如下:

class Solution {
public:
    vector<int> sortArrayByParityII(vector<int>& A) {
        vector<int> even(A.size() / 2); // 初始化就确定数组大小,节省开销
        vector<int> odd(A.size() / 2);
        vector<int> result(A.size());
        int evenIndex = 0;
        int oddIndex = 0;
        int resultIndex = 0;
        // 把A数组放进偶数数组,和奇数数组
        for (int i = 0; i < A.size(); i++) {
            if (A[i] % 2 == 0) even[evenIndex++] = A[i];
            else odd[oddIndex++] = A[i];
        }
        // 把偶数数组,奇数数组分别放进result数组中
        for (int i = 0; i < evenIndex; i++) {
            result[resultIndex++] = even[i];
            result[resultIndex++] = odd[i];
        }
        return result;
    }
};
  • 时间复杂度:O(n)
  • 空间复杂度:O(n)

方法二

以上代码我是建了两个辅助数组,而且A数组还相当于遍历了两次,用辅助数组的好处就是思路清晰,优化一下就是不用这两个辅助树,代码如下:

class Solution {
public:
    vector<int> sortArrayByParityII(vector<int>& A) {
        vector<int> result(A.size());
        int evenIndex = 0;  // 偶数下标
        int oddIndex = 1;   // 奇数下标
        for (int i = 0; i < A.size(); i++) {
            if (A[i] % 2 == 0) {
                result[evenIndex] = A[i];
                evenIndex += 2;
            }
            else {
                result[oddIndex] = A[i];
                oddIndex += 2;
            }
        }
        return result;
    }
};
  • 时间复杂度:O(n)
  • 空间复杂度:O(n)

方法三

当然还可以在原数组上修改,连result数组都不用了。

class Solution {
public:
    vector<int> sortArrayByParityII(vector<int>& A) {
        int oddIndex = 1;
        for (int i = 0; i < A.size(); i += 2) {
            if (A[i] % 2 == 1) { // 在偶数位遇到了奇数
                while(A[oddIndex] % 2 != 0) oddIndex += 2; // 在奇数位找一个偶数
                swap(A[i], A[oddIndex]); // 替换
            }
        }
        return A;
    }
};
  • 时间复杂度:O(n)
  • 空间复杂度:O(1)

这里时间复杂度并不是O(n^2),因为偶数位和奇数位都只操作一次,不是n/2 * n/2的关系,而是n/2 + n/2的关系!这个方法并不很容易想明白,但是空间最低。

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

24. 两两交换链表中的节点 - 力扣(Leetcode)

并不是新题目,复习以前的,结果还是没一次ac,多复习吧,不然又忘了。因为是单链表,要点依然是记得在链接断开前记录关键节点,以及不要把操作的顺序搞错。

思路

这道题目正常模拟就可以了。

建议使用虚拟头结点,这样会方便很多,要不然每次针对头结点(没有前一个指针指向头结点),还要单独处理。

接下来就是交换相邻两个元素了,此时一定要画图,不画图,操作多个指针很容易乱,而且要操作的先后顺序

初始时,cur指向虚拟头结点,然后进行如下三步:

在这里插入图片描述

操作之后,链表如下:

在这里插入图片描述

看这个可能就更直观一些了:

在这里插入图片描述

对应的C++代码实现如下: (注释中详细和如上图中的三步做对应)

class Solution {
public:
    ListNode* swapPairs(ListNode* head) {
        ListNode* dummyHead = new ListNode(0); // 设置一个虚拟头结点
        dummyHead->next = head; // 将虚拟头结点指向head,这样方面后面做删除操作
        ListNode* cur = dummyHead;
        while(cur->next != nullptr && cur->next->next != nullptr) {
            ListNode* tmp = cur->next; // 记录临时节点
            ListNode* tmp1 = cur->next->next->next; // 记录临时节点

            cur->next = cur->next->next;    // 步骤一
            cur->next->next = tmp;          // 步骤二
            cur->next->next->next = tmp1;   // 步骤三

            cur = cur->next->next; // cur移动两位,准备下一轮交换
        }
        return dummyHead->next;
    }
};
  • 时间复杂度:O(n)
  • 空间复杂度:O(1)

我自己更倾向于这种写法,主要是可以少些几个next:

class Solution {
public:
    ListNode* swapPairs(ListNode* head) {
        if (head == nullptr){
            return head;
        }
        ListNode * dummyHead = new ListNode(0);
        dummyHead->next = head;
        ListNode * cur = dummyHead;

        while (cur -> next != nullptr && cur->next->next != nullptr) {
            ListNode * left = cur->next;
            ListNode * right = cur->next->next;

            left->next = right->next;
            right -> next = left;
            cur -> next = right;
            cur = left;
        }
        return dummyHead->next;
    }
};

234.回文链表

234. 回文链表 - 力扣(Leetcode)

总体来书方法不是很新颖,但是比较考验基本功,重点是如何反转链表,不出意外的话又忘了,及时复习吧。

思路

数组模拟

最直接的想法,就是把链表装成数组,然后再判断是否回文。

代码也比较简单。如下:

class Solution {
public:
    bool isPalindrome(ListNode* head) {
        vector<int> vec;
        ListNode* cur  = head;
        while (cur) {
            vec.push_back(cur->val);
            cur = cur->next;
        }
        // 比较数组回文
        for (int i = 0, j = vec.size() - 1; i < j; i++, j--) {
            if (vec[i] != vec[j]) return false;
        }
        return true;
    }
};

上面代码可以在优化,就是先求出链表长度,然后给定vector的初始长度,这样避免vector每次添加节点重新开辟空间

class Solution {
public:
    bool isPalindrome(ListNode* head) {

        ListNode* cur  = head;
        int length = 0;
        while (cur) {
            length++;
            cur = cur->next;
        }
        vector<int> vec(length, 0); // 给定vector的初始长度,这样避免vector每次添加节点重新开辟空间
        cur = head;
        int index = 0;
        while (cur) {
            vec[index++] = cur->val;
            cur = cur->next;
        }
        // 比较数组回文
        for (int i = 0, j = vec.size() - 1; i < j; i++, j--) {
            if (vec[i] != vec[j]) return false;
        }
        return true;
    }
};

反转后半部分链表

分为如下几步:

  • 用快慢指针,快指针有两步,慢指针走一步,快指针遇到终止位置时,慢指针就在链表中间位置
  • 同时用pre记录慢指针指向节点的前一个节点,用来分割链表
  • 将链表分为前后均等两部分,如果链表长度是奇数,那么后半部分多一个节点
  • 将后半部分反转 ,得cur2,前半部分为cur1
  • 按照cur1的长度,一次比较cur1和cur2的节点数值

如图所示:

在这里插入图片描述

代码如下:

class Solution {
public:
    bool isPalindrome(ListNode* head) {
        if (head == nullptr || head->next == nullptr) return true;
        ListNode* slow = head; // 慢指针,找到链表中间分位置,作为分割
        ListNode* fast = head;
        ListNode* pre = head; // 记录慢指针的前一个节点,用来分割链表
        while (fast && fast->next) {
            pre = slow;
            slow = slow->next;
            fast = fast->next->next;
        }
        pre->next = nullptr; // 分割链表

        ListNode* cur1 = head;  // 前半部分
        ListNode* cur2 = reverseList(slow); // 反转后半部分,总链表长度如果是奇数,cur2比cur1多一个节点

        // 开始两个链表的比较
        while (cur1) {
            if (cur1->val != cur2->val) return false;
            cur1 = cur1->next;
            cur2 = cur2->next;
        }
        return true;
    }
    // 反转链表
    ListNode* reverseList(ListNode* head) {
        ListNode* temp; // 保存cur的下一个节点
        ListNode* cur = head;
        ListNode* pre = nullptr;
        while(cur) {
            temp = cur->next;  // 保存一下 cur的下一个节点,因为接下来要改变cur->next
            cur->next = pre; // 翻转操作
            // 更新pre 和 cur指针
            pre = cur;
            cur = temp;
        }
        return pre;
    }
};

35.搜索插入位置

35. 搜索插入位置 - 力扣(Leetcode)

二分查找的另一个应用,关键就是搞清楚边界。对于左闭右闭的区间里,也就是[left, right]来说,因为最后left会等于right,而mid值是向下取整的,所以会落在最后一个小于target的位置上,则插入位置就是right+1;对于左闭右开的区间里,也就是[left, right)来说,left最终会小于right,因为则在left和right区间中就是插入target的位置,自然此时就是right位置插入了。

思路

这道题目不难,但是为什么通过率相对来说并不高呢,我理解是大家对边界处理的判断有所失误导致的。

这道题目,要在数组中插入目标值,无非是这四种情况。

在这里插入图片描述

  • 目标值在数组所有元素之前
  • 目标值等于数组中某一个元素
  • 目标值插入数组中的位置
  • 目标值在数组所有元素之后

这四种情况确认清楚了,就可以尝试解题了。

接下来我将从暴力的解法和二分法来讲解此题,也借此好好讲一讲二分查找法。

暴力解法

暴力解题 不一定时间消耗就非常高,关键看实现的方式,就像是二分查找时间消耗不一定就很低,是一样的。

C++代码

class Solution {
public:
    int searchInsert(vector<int>& nums, int target) {
        for (int i = 0; i < nums.size(); i++) {
        // 分别处理如下三种情况
        // 目标值在数组所有元素之前
        // 目标值等于数组中某一个元素
        // 目标值插入数组中的位置
            if (nums[i] >= target) { // 一旦发现大于或者等于target的num[i],那么i就是我们要的结果
                return i;
            }
        }
        // 目标值在数组所有元素之后的情况
        return nums.size(); // 如果target是最大的,或者 nums为空,则返回nums的长度
    }
};
  • 时间复杂度:O(n)
  • 空间复杂度:O(1)

二分法

既然暴力解法的时间复杂度是 O ( n ) O(n) O(n),就要尝试一下使用二分查找法。

在这里插入图片描述

大家注意这道题目的前提是数组是有序数组,这也是使用二分查找的基础条件。

以后大家只要看到面试题里给出的数组是有序数组,都可以想一想是否可以使用二分法。

同时题目还强调数组中无重复元素,因为一旦有重复元素,使用二分查找法返回的元素下标可能不是唯一的。

大体讲解一下二分法的思路,这里来举一个例子,例如在这个数组中,使用二分法寻找元素为5的位置,并返回其下标。

在这里插入图片描述

二分查找涉及的很多的边界条件,逻辑比较简单,就是写不好。

相信很多同学对二分查找法中边界条件处理不好。

例如到底是 while(left < right) 还是 while(left <= right),到底是right = middle呢,还是要right = middle - 1呢?

这里弄不清楚主要是因为对区间的定义没有想清楚,这就是不变量

要在二分查找的过程中,保持不变量,这也就是循环不变量 (感兴趣的同学可以查一查)。

二分法第一种写法

以这道题目来举例,以下的代码中定义 target 是在一个在左闭右闭的区间里,也就是[left, right] (这个很重要)

这就决定了这个二分法的代码如何去写,大家看如下代码:

大家要仔细看注释,思考为什么要写while(left <= right), 为什么要写right = middle - 1

class Solution {
public:
    int searchInsert(vector<int>& nums, int target) {
        int n = nums.size();
        int left = 0;
        int right = n - 1; // 定义target在左闭右闭的区间里,[left, right]
        while (left <= right) { // 当left==right,区间[left, right]依然有效
            int middle = left + ((right - left) / 2);// 防止溢出 等同于(left + right)/2
            if (nums[middle] > target) {
                right = middle - 1; // target 在左区间,所以[left, middle - 1]
            } else if (nums[middle] < target) {
                left = middle + 1; // target 在右区间,所以[middle + 1, right]
            } else { // nums[middle] == target
                return middle;
            }
        }
        // 分别处理如下四种情况
        // 目标值在数组所有元素之前  [0, -1]
        // 目标值等于数组中某一个元素  return middle;
        // 目标值插入数组中的位置 [left, right],return  right + 1
        // 目标值在数组所有元素之后的情况 [left, right], 因为是右闭区间,所以 return right + 1
        return right + 1;
    }
};
  • 时间复杂度:O(log n)
  • 空间复杂度:O(1)

二分法第二种写法

如果说定义 target 是在一个在左闭右开的区间里,也就是[left, right) 。

那么二分法的边界处理方式则截然不同。

不变量是[left, right)的区间,如下代码可以看出是如何在循环中坚持不变量的。

大家要仔细看注释,思考为什么要写while (left < right), 为什么要写right = middle

class Solution {
public:
    int searchInsert(vector<int>& nums, int target) {
        int n = nums.size();
        int left = 0;
        int right = n; // 定义target在左闭右开的区间里,[left, right)  target
        while (left < right) { // 因为left == right的时候,在[left, right)是无效的空间
            int middle = left + ((right - left) >> 1);
            if (nums[middle] > target) {
                right = middle; // target 在左区间,在[left, middle)中
            } else if (nums[middle] < target) {
                left = middle + 1; // target 在右区间,在 [middle+1, right)中
            } else { // nums[middle] == target
                return middle; // 数组中找到目标值的情况,直接返回下标
            }
        }
        // 分别处理如下四种情况
        // 目标值在数组所有元素之前 [0,0)
        // 目标值等于数组中某一个元素 return middle
        // 目标值插入数组中的位置 [left, right) ,return right 即可
        // 目标值在数组所有元素之后的情况 [left, right),因为是右开区间,所以 return right
        return right;
    }
};
  • 时间复杂度: O ( log ⁡ n ) O(\log n) O(logn)
  • 时间复杂度: O ( 1 ) O(1) O(1)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值