移除元素
例题27:给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。
不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并 原地 修改输入数组。
元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
//使用左右指针,如果左指针为val才与右指针交换,两指针移动,如果不等则只移动左指针
int left = 0, right = nums.size() - 1;
while (left <= right)
{
if (nums[left] == val && nums[right]!=val)
{
swap(nums[left], nums[right]);
nums.resize(nums.size()-1);
right--;
left++;
}
else if (nums[left] == val && nums[right] == val)
{
right--;
nums.resize(nums.size()-1);
}
else{
left++;
}
}
return nums.size();
}
};
采用双指针的方法,从左至右逼近容器数组,两种指针分为三种情况:
1)如果两者都为val,则右指针前移一位,左指针不动;
2)如果左指针为val,右不是,则交换两者元素位置,再分别前移后移一位;
3)如果左右都不是val,则左指针前移,右指针不动
该算法时间复杂度为O(n)
例题26:给你一个 升序排列 的数组 nums ,请你 原地 删除重复出现的元素,使每个元素 只出现一次 ,返回删除后数组的新长度。元素的 相对顺序 应该保持 一致 。然后返回 nums 中唯一元素的个数。
考虑 nums 的唯一元素的数量为 k ,你需要做以下事情确保你的题解可以被通过:
更改数组 nums ,使 nums 的前 k 个元素包含唯一元素,并按照它们最初在 nums 中出现的顺序排列。nums 的其余元素与 nums 的大小不重要。
返回 k 。nums已按升序排列
class Solution {
public:
int removeDuplicates(vector<int>& nums) {
//同时开始快慢指针,如果快指针与慢指针相同,则找到不相同的位置从后往前覆盖
int low = 0,fast=1,i;
while (low < nums.size()-1)
{
while (nums[fast] == nums[low] )
{
if(fast==nums.size()-1)
{
nums.resize(nums.size()-(fast-low));
return nums.size();
}
else
fast++;
}
int count=fast-low;
if ( count>1)//不相同有重复
{
i = fast;
while (i < nums.size())//覆盖
{
nums[i - (count - 1)] = nums[i];
i++;
}
nums.resize(nums.size() - (count- 1));
low++;
fast = low + 1;
}
else if(count == 1)//不相同无重复
{
low++;
fast = low + 1;
}
}
return nums.size();
}
};
该方法快指针找到新种类元素时,快指针倒退,增加了比较次数,可以进行改进如下:
使用快慢指针,慢指针用来存放当前元素的种类与位置,快指针遍历容器查找下一个种类,当找到第一个不同种类时就替换为慢指针的后一位;
当快指针遍历完就结束循环
例题283:给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。
请注意 ,必须在不复制数组的情况下原地对数组进行操作。
void moveZores(vector<int>& nums)
{
int i = 0;
int count = nums.size();
while (count>0)
{
if (nums[i] == 0)
{
int low = i ;
int fast = i + 1;
while (fast < nums.size())
{
nums[low] = nums[fast];
low++;
fast++;
}
nums[nums.size() - 1] = 0;
}
else{
i++;
}
count--;
}
}
在一层循环中嵌套快慢指针,如果找到元素为0,那么将后面的元素依次迁移,再将最后一个元素置零;
例题844:给定 s 和 t 两个字符串,当它们分别被输入到空白的文本编辑器后,如果两者相等,返回 true 。# 代表退格字符。
注意:如果对空文本输入退格字符,文本继续为空。
class Solution {
public:
string comparebackspace(string s)//得到退格后的字符串函数
{
int low=0,fast=1;
while(low<s.size())
{
if(s[low]=='#' && low==0)//当第一位为退回符时,字符串整体前移一位,字符串长度减一
{
while(fast<s.size())
{
s[fast-1]=s[fast];
fast++;
}
fast=1;
s.resize(s.size()-1);
}
else if(s[low]=='#' && low!=0)//当中间位为退回符时,从后往前覆盖退回符和其前一位,字符串长度减二
{
while(fast<s.size())
{
s[fast-2]=s[fast];
fast++;
}
low=low-1;
fast=low+1;
s.resize(s.size()-2);
}
else if(s[low]!='#')
{
low++;
fast++;
}
}
return s;
}
bool backspaceCompare(string s, string t) {
string ress,rest;
ress=comparebackspace(s);
rest=comparebackspace(t);
if(ress==rest)
{
return true;
}
else {
return false;
}
}
};
使用的方法是构造一个函数得到退格后的字符串,再比较两字符串退格后是否相等
得到退格字符串的方法是:用双指针遍历字符串,如果慢指针为退格符,则需删去退格符及其前一字符,也就是将字符串从后往前移动两位
- 该方法可以进行简化:使用栈处理遍历过程,每次遍历一个字符:
- 如果该字符不是’#',则压入栈;
- 否则,将栈顶弹出。
简化具体代码如下:
class Solution {
public:
bool backspaceCompare(string S, string T) {
return build(S) == build(T);
}
string build(string str) {
string ret;
for (char ch : str) {
if (ch != '#') {
ret.push_back(ch);
} else if (!ret.empty()) {
ret.pop_back();
}
}
return ret;
}
};
例题977:给你一个按 非递减顺序 排序的整数数组 nums,返回 每个数字的平方 组成的新数组,要求也按 非递减顺序 排序。
class Solution {
public:
vector<int> sortedSquares(vector<int>& nums) {
//先平方后排序,时间复杂度最低为快排的O(nlgn)
//用双指针指向一头一尾,将绝对值更大元素的平方插入新数组的头部,然后移动该数的指针,另一个不动,两端逼近为止
vector<int> res;
int left = 0, right = nums.size() - 1;
while (left <= right)
{
int max;
max = abs(nums[left]) >= abs(nums[right]) ? left : right;
res.insert(res.begin(), nums[max]*nums[max]);//头插法res.push_front();
if (max == left)
{
left++;
}
else if(max==right)
{
right--;
}
}
return res;
}
};
反转字符串
例题344:编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 s 的形式给出。
不要给另外的数组分配额外的空间,你必须原地修改输入数组、使用 O(1) 的额外空间解决这一问题。
void reverseString(vector<char>& s) {
//头尾指针向中间逼近交换元素
int left = 0, right = s.size() - 1;
while (left <= right)
{
swap(s[left], s[right]);
left++;
right--;
}
}
替换空格
例题剑指offer 05题:请实现一个函数,把字符串 s 中的每个空格替换成"%20"。
string replaceSpace1(string s) {
//开新空间:①遍历字符串,碰到空格在新字符串后加上%20,否则填入该元素本身
// ②利用栈解决:从后往前将元素推入栈,如果碰到空格则推入%20,否则推入原字符
// ③先找到空格个数,将原字符串扩充到替换空格后的大小,碰到空格的话,将字符串从该位置往后移2位,再填入%20
stack<char> sta;
for (int i=s.size()-1;i>=0;i--)
{
if (s[i] == ' ')
{
sta.push('0');
sta.push('2');
sta.push('%');
}
else
{
sta.push(s[i]);
}
}
string res;
while (!sta.empty())
{
char t = sta.top();
res.push_back(t);
sta.pop();
}
return res;
}
翻转字符里的单词
例题151:给你一个字符串 s ,请你反转字符串中 单词 的顺序。
单词 是由非空格字符组成的字符串。s 中使用至少一个空格将字符串中的 单词 分隔开。
返回 单词 顺序颠倒且 单词 之间用单个空格连接的结果字符串。
注意:输入字符串 s中可能会存在前导空格、尾随空格或者单词间的多个空格。返回的结果字符串中,单词间应当仅用单个空格分隔,且不包含任何额外的空格。
class Solution {
public:
string reverseWords(string s) {
//重开空间的做法
if(s.size()==1)
{
return s;
}
string res;
int right = s.size() - 1;
int left = right;
int t;
while(left>0)
{
while (right>0 && s[right] == ' ' )
{
right--;
if(right==0 && s[right]==' ')
{
res.erase(res.size()-1);
return res;
}
}
left = right;
while (left>0 && s[left - 1] != ' ')
{
left--;
}
t = left-1;
while (t < right)
{
res.push_back(s[++t]);
}
res.push_back(' ');
right = left-1;
}
while(res[res.size()-1]==' ')
{
res.resize(res.size()-1);
}
return res;
}
};
重开空间做法:从后往前依次找到每个单词将其翻转后存入新字符串,在每个单词后加一个空格,最后删去末尾的空格
翻转链表
例题206:给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。
ListNode* prehead = nullptr;
ListNode* pre = prehead;
ListNode* cur=head;
ListNode* behind;
while (cur != nullptr)
{
behind =cur->next;//behind会超出链表到nullptr
cur->next = pre;
pre = cur;
cur = behind;
}
return pre;
}
};
交换两节点位置时顺序不能换,否则cur先后移一位,pre也到了cur后移后的位置
还可以使用虚结点的头插法将原链表的节点从头至尾插入,实现链表翻转
使用栈:先将链表入栈,然后用虚结点依次连接出栈的元素
// 迭代方法:增加虚头结点,使用头插法实现链表翻转
public static ListNode reverseList1(ListNode head) {
// 创建虚头结点
ListNode dumpyHead = new ListNode(-1);
dumpyHead.next = null;
// 遍历所有节点
ListNode cur = head;
while(cur != null){
ListNode temp = cur.next;
// 头插法
cur.next = dumpyHead.next;
dumpyHead.next = cur;
cur = temp;
}
return dumpyHead.next;
}
使用栈实现链表翻转
public ListNode reverseList(ListNode head) {
// 如果链表为空,则返回空
if (head == null) return null;
// 如果链表中只有只有一个元素,则直接返回
if (head.next == null) return head;
// 创建栈 每一个结点都入栈
Stack<ListNode> stack = new Stack<>();
ListNode cur = head;
while (cur != null) {
stack.push(cur);
cur = cur.next;
}
// 创建一个虚拟头结点
ListNode pHead = new ListNode(0);
cur = pHead;
while (!stack.isEmpty()) {
ListNode node = stack.pop();
cur.next = node;
cur = cur.next;
}
// 最后一个元素的next要赋值为空
cur.next = null;
return pHead.next;
}
删除链表的倒数第N个节点
例题19:给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
//删除的倒数第n个,也就是正数第sz-n+1个
ListNode* dummyHead= new ListNode();
dummyHead->next = head;
ListNode* left = dummyHead;
ListNode* right = head;
int sz = 0;
while (right != nullptr)//找到链表的节点数
{
sz++;
right = right->next;
}
right = head;
while (sz!=n)//找到要删除的倒数第n个节点,此时left指向其前一个,right指向倒数第n个
{
left = left->next;
right = right->next;
sz--;
}
left->next = right->next;
delete right;
return dummyHead->next;
}
};
进阶版:使用一次遍历
定义快慢指针指向虚头节点,然后让fast先走n+1步,这时再让slow和fast同步走,当fast走到nullptr时,slow走到了要删除节点的前一位,直接删除倒数第n个节点即可
为什么要走n+1步?只有这样同时移动时slow才能指向要删除节点的前一位,方便做删除操作
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode* dummyHead = new ListNode(0);
dummyHead->next = head;
ListNode* slow = dummyHead;
ListNode* fast = dummyHead;
while(n-- && fast != NULL) {
fast = fast->next;
}
fast = fast->next; // fast再提前走一步,因为需要让slow指向删除节点的上一个节点
while (fast != NULL) {
fast = fast->next;
slow = slow->next;
}
slow->next = slow->next->next;
// ListNode *tmp = slow->next; C++释放内存的逻辑
// slow->next = tmp->next;
// delete nth;
return dummyHead->next;
}
};
链表相交
例题160:给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null 。
图示两个链表在节点 c1 开始相交:
//算出两链表的长度差,让长链表指向头节点的指针先走差这么多位,然后两指针同时走,直到找到第一个地址相同的点为相交点
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
ListNode* dummyHeadA = new ListNode(-1);
dummyHeadA->next = headA;
ListNode* dummyHeadB = new ListNode(-1);
dummyHeadB->next = headB;
ListNode* p = dummyHeadA;
ListNode* q = dummyHeadB;
int lengthA = -1;
int lengthB = -1;
while (q != nullptr)
{
lengthB++;
q=q->next;
}
while (p != nullptr)
{
lengthA++;
p=p->next;
}
p = dummyHeadA->next;
q = dummyHeadB->next;
int dis = (lengthA - lengthB) >= 0 ? (lengthA - lengthB) : (lengthB - lengthA);
if (lengthA >= lengthB)
{
while (dis > 0)
{
p = p->next;
dis--;
}
}
else
{
while (dis > 0)
{
q = q->next;
dis--;
}
}
while (q != nullptr && p != nullptr)
{
if (q == p)
{
return q;
}
q = q->next;
p = p->next;
}
return nullptr;
}
};
该方法时间复杂度为O(n+m),空间复杂度为O(1)
环形链表
例题142:给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。
不允许修改 链表。
ListNode *detectCycle(ListNode *head) {
unordered_map<ListNode*, int> hmap;
ListNode* dummyHead = new ListNode();
dummyHead->next = head;
ListNode* p = dummyHead;
while (p != nullptr)
{
hmap[p]++;
if (hmap[p] > 1)
{
return p;
}
p = p->next;
}
return nullptr;
}
};
该方法的进阶版:使用空间复杂度为O(1)的方法
具体实现:1.判断是否有环:使用快慢指针,快指针每次走两步,慢指针每次走一步,如果两者相遇,则一定是在环内相遇,该点为相遇点(为什么快走2,慢走1两者一定会相遇?)
2.有环后怎么找到环入口:列方程,当相遇时,慢指针一共走了x+y步,快指针一共走了x+y+n(y+z)步,而快又是慢的两倍,所以
2(x+y)=x+y+n(y+z) ——>可以发现,当n=1时,x=z,也就是说,在相遇的位置设一个指针index,在头指针设一个慢指针,两者同时走,相遇时就为环入口;n>1时也相同
//进阶版
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
ListNode* fast = head;
ListNode* slow = head;
while(fast != NULL && fast->next != NULL) {
slow = slow->next;
fast = fast->next->next;
// 快慢指针相遇,此时从head 和 相遇点,同时查找直至相遇
if (slow == fast) {
ListNode* index1 = fast;
ListNode* index2 = head;
while (index1 != index2) {
index1 = index1->next;
index2 = index2->next;
}
return index2; // 返回环的入口
}
}
return NULL;
}
};
三数之和(较难)
例题15:给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != j、i != k 且 j != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0 。请
你返回所有和为 0 且不重复的三元组。
注意:答案中不可以包含重复的三元组。
vector<vector<int>> threeSum(vector<int>& nums) {
//先排序,在一层for循环下,用双指针:如果三者之和大于0,则将右指针前移,否则将左指针后移
sort(nums.begin(), nums.end());
vector<vector<int>> res;
int i, left, right;
for (i = 0; i < nums.size(); i++)
{
if(i>0 && nums[i]==nums[i-1])//去重的关键步骤:跳过需要判断的和数
{
continue;
}
left = i + 1;
right = nums.size() - 1;
while (left < right)
{
if (nums[i] + nums[left] + nums[right] > 0)
{
right--;
}
else if (nums[i] + nums[left] + nums[right] < 0)
{
left++;
}
else
{
res.push_back(vector<int>{nums[i], nums[left], nums[right]});
while (left < right && nums[right] == nums[right - 1])//去重的关键步骤:在一个固定的数下,跳过可能重复的和组合元素
{
right--;
}
while (left < right && nums[left] == nums[left + 1])
{
left++;
}
right--;
left++;
}
}
}
return res;
}
};
四数之和
例题18:给你一个由 n 个整数组成的数组 nums ,和一个目标值 target 。请你找出并返回满足下述全部条件且不重复的四元组 [nums[a], nums[b], nums[c], nums[d]] (若两个四元组元素一一对应,则认为两个四元组重复):
0 <= a, b, c, d < n
a、b、c 和 d 互不相同
nums[a] + nums[b] + nums[c] + nums[d] == target
你可以按 任意顺序 返回答案 。
//两两互为相反数
sort(nums.begin(), nums.end());
vector<vector<int>> res;
int n1, n2, left, right;
for (n1 = 0; n1 < nums.size(); n1++)
{
//对n1剪枝
if(nums[n1]>=0 && nums[n1]>target)
{
break;
}
//对n1去重
if (n1 > 0 && nums[n1] == nums[n1 - 1])
{
continue;
}
for (n2 = n1 + 1; n2 < nums.size(); n2++)
{
//对n2剪枝:注意正数下大于目标才剪枝,负数下继续遍历
if(nums[n1]+nums[n2]>=0 && nums[n2]+nums[n1]>target)
{
break;
}
//对n2去重
if (n2 > n1+1 && nums[n2] == nums[n2 - 1])//去重不正确的原因在这里,n2的范围不对,会导致n1=n2时也被去掉
{
continue;
}
double sum = nums[n1] + nums[n2];
double sum2 = target - sum;
left = n2 + 1;
right = nums.size() - 1;
while (left < right)
{
if (nums[left] + nums[right] > sum2)
{
right--;
}
else if (nums[left] + nums[right] < sum2)
{
left++;
}
else
{
vector<int> a = { nums[n1], nums[n2], nums[left], nums[right] };
sort(a.begin(),a.end());
res.push_back(a);
while (left < right && nums[right] == nums[right - 1])
{
right--;
}
while (left < right && nums[left] == nums[left + 1])
{
left++;
}
right--;
left++;
}
}
}
}
return res;
}
};
思路与三数之和一样,难点在于怎么对前两位数去重?
双指针总结
数组篇
移除元素或翻转数组,用双指针在一个for循环下完成两个for循环的工作
字符串篇
erase()复杂度为O(n)
链表篇
N数之和篇
双指针主要用在后两数之和是否等于前面数之和的相反数,注意每次前后移动要去重
三数之和使用双指针,将复杂度从O(n^3) 降为O(n*n),同理四数之和也是如此