本题进一步练习二分法区间收窄时的细节控制,及分别使用两个函数(或一个函数两个判断条件)寻找左右位置的整体思路。
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};
}
};
比较经典的一道题目,之前做过。
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++;
}
}
};
思路类似标准题。
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);
}
};
注意释放内存:
ListNode* tmp = cur->next;
cur->next = cur->next->next;
delete tmp;
不使用虚拟头节点时,头指针需要和后面的循环分开判断。
使用虚拟头节点:
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;
}
照着网站上的答案敲了一遍,涉及到index与size判断的地方,有时思路不够清晰,容易马虎。对于一个链表结构所包含的节点结构体、哑头、链表大小及常规操作需要再熟悉。
首先比较简单直接的想法是,想要反转某一个节点,则必须同时记录其前面一个节点和后面一个节点,算上它自己,共三个节点。对于链表长度不足三个节点的情况,分情况讨论:
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;
}
};
上面的方法显然很不简练,下面考虑对不同情况进行归并。
整理后的代码如下:
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;
}
};
反思:将当前节点作为中心,让它遍历整个链表的每一个节点,以这种思路去切入,可能会得到更通用和精简的解答。
数组用花括号表示,如 {-1, 2, 3}。
还没上道儿。