代码随想录拓展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)