一.双指针
思想:双指针主要用于遍历数组,两个指针指向不同的元素,从而协同完成任务。
1.排序数组的两数之和(easy 167)
给定一个已按照升序排列 的有序数组,找到两个数使得它们相加之和等于目标数。
函数应该返回这两个下标值 index1 和 index2,其中 index1 必须小于 index2。
说明:
返回的下标值(index1 和 index2)不是从零开始的。
你可以假设每个输入只对应唯一的答案,而且你不可以重复使用相同的元素。
//O(n) O(1)
//使用双指针,一个指针指向值较小的元素,一个指针指向值较大的元素。
//指向较小元素的指针从头向尾遍历,指向较大元素的指针从尾向头遍历。
//如果两个指针指向元素的和 sum == target,那么得到要求的结果;
//如果 sum > target,移动较大的元素,使 sum 变小一些;
//如果 sum < target,移动较小的元素,使 sum 变大一些。
class Solution {
public:
vector<int> twoSum(vector<int>& numbers, int target) {
vector<int> res;
int le=0,ri=numbers.size()-1;
while(le<ri){
int sum=numbers[ri]+numbers[le];
if(sum==target){
res.push_back(le+1);
res.push_back(ri+1);
return res;
}
else if(sum<target){
le++;
}
else{
ri--;
}
}
return res;
}
};
2.平方数之和(easy 633)
给定一个非负整数 c ,你要判断是否存在两个整数 a 和 b,使得 a2 + b2 = c。
//思路与上题一致 j初始设置为c^2
//注意i j sum都用long
class Solution {
public:
bool judgeSquareSum(int c) {
if (c< 0) {
return false;}
long i = 0, j = sqrt(c);
while (i <= j) {
long powSum = i * i + j * j;
if (powSum == c) {
return true;
} else if (powSum > c) {
j--;
} else {
i++;
}
}
return false;
}
};
3.反转字符串中的元音字母
编写一个函数,以字符串作为输入,反转该字符串中的元音字母。
示例 1:
输入: “hello”
输出: “holle”
示例 2:
输入: “leetcode”
输出: “leotcede”
说明:
元音字母不包含字母"y"。
//双指针,遇见元音交换
class Solution {
public:
string reverseVowels(string s) {
set<char> vowels={
'a', 'e', 'i', 'o', 'u', 'A', 'E', 'I', 'O', 'U'};
int left=0,right=s.size()-1;
while(left<=right){
char cl=s[left];
char cr=s[right];
if(!vowels.count(cl)){
left++;
}
else if(!vowels.count(cr)){
right--;
}
else{
s[left]=cr;
s[right]=cl;
left++;
right--;
}
}
return s;
}
};
4.验证回文字符串II(easy 680)
给定一个非空字符串 s,最多删除一个字符。判断是否能成为回文字符串。
示例 1:
输入: “aba”
输出: True
示例 2:
输入: “abca”
输出: True
解释: 你可以删除c字符。
//O(n) O(1)
//双指针判断两边是否相等,遇到不相等,试着删除一个i或j来继续判断是否回文
class Solution {
public:
bool validPalindrome(string s) {
for(int i=0,j=s.size()-1;i<j;i++,j--){
if(s[i]!=s[j]){
return isPalindrome(s,i+1,j)||isPalindrome(s,i,j-1);
}
}
return true;
}
bool isPalindrome(string s,int i,int j){
while(i<j){
if(s[i++]!=s[j--]){
return false;
}
}
return true;
}
};
5.合并两个有序数组(easy 88)
给你两个有序整数数组 nums1 和 nums2,请你将 nums2 合并到 nums1 中,使 num1 成为一个有序数组。
说明:
初始化 nums1 和 nums2 的元素数量分别为 m 和 n 。
你可以假设 nums1 有足够的空间(空间大小大于或等于 m + n)来保存 nums2 中的元素。
示例:
输入:
nums1 = [1,2,3,0,0,0], m = 3
nums2 = [2,5,6], n = 3
解法一:
一般而言,对于有序数组可以通过 双指针法 达到O(n + m)的时间复杂度。
最直接的算法实现是将指针p1 置为 nums1的开头, p2为 nums2的开头,在每一步将最小值放入输出数组中。
由于 nums1 是用于输出的数组,需要将nums1中的前m个元素放在其他地方,也就需要 O(m)的空间复杂度。
class Solution {
public:
void merge(vector<int>& nums1, int m, vector<int>& nums2, int n) {
vector<int> numscp1=nums1;
int i=0,j=0,p=0;
while(i<m&& j<n){
nums1[p++] = (numscp1[i] < nums2[j]) ? numscp1[i++] : nums2[j++];
}
while(i<m){
nums1[p++]=numscp1[i++];
}
while(j<n){
nums1[p++]=nums2[j++];
}
}
};
解法二:
解法一的延伸版:如果从结尾开始改写 nums1 的值这里没有信息,因此不需要额外空间。
//Java版本
class Solution {
public void merge(int[] nums1, int m, int[] nums2, int n) {
// two get pointers for nums1 and nums2
int p1 = m - 1;
int p2 = n - 1;
// set pointer for nums1
int p = m + n - 1;
// while there are still elements to compare
while ((p1 >= 0) && (p2 >= 0))
// compare two elements from nums1 and nums2
// and add the largest one in nums1
nums1[p--] = (nums1[p1] < nums2[p2]) ? nums2[p2--] : nums1[p1--];
// add missing elements from nums2
System.arraycopy(nums2, 0, nums1, 0, p2 + 1);
}
}
6.判断环形链表
给定一个链表,判断链表中是否有环。
为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。
思路一:
使用两个slow, fast指针从头开始扫描链表。指针slow 每次走1步,指针fast每次走2步。如果存在环,则指针slow、fast会相遇;如果不存在环,指针fast遇到NULL退出。。
放一个证明 证明此方法判断单链表有环的正确性
//O(n) O(1)
class Solution {
public:
bool hasCycle(ListNode *head) {
if(head==NULL){
return false;}
ListNode* l1=head;
ListNode* l2=head->next;
while(l1!=NULL && l2!=NULL && l2->next!=NULL){
if(l1==l2){
return true;
}
l1=l1->next;
l2=l2->next->next;
}
return false;
}
};
思路2:
遍历所有结点并在哈希表中存储每个结点的引用(或内存地址)。如果当前结点为空结点 null(即已检测到链表尾部的下一个结点),那么我们已经遍历完整个链表,并且该链表不是环形链表。如果当前结点的引用已经存在于哈希表中,那么返回 true(即该链表为环形链表)。
//O(n) O(1)
class Solution {
public:
bool hasCycle(ListNode *head) {
set<ListNode*> s;
while(head!=NULL){
if(s.count(head)){
return true;
}
else{
s.insert(head);
}
head=head->next;
}
return false;
}
};
7.通过删除字母匹配到字典中最长的单词(medium 524)
给定一个字符串和一个字符串字典,找到字典里面最长的字符串,该字符串可以通过删除给定字符串的某些字符来得到。如果答案不止一个,返回长度最长且字典顺序最小的字符串。如果答案不存在,则返回空字符串。
示例 1:
输入:
s = “abpcplea”, d = [“ale”,“apple”,“monkey”,“plea”]
输出:
“apple”
思路:
其实是一个最长子序列问题。通过删除字符串 s 中的一个字符能得到字符串 t,可以认为 t 是 s 的子序列,我们可以使用双指针来判断一个字符串是否为另一个字符串的子序列。
class Solution {
public:
//前提是可以通过删除s中得字符,直接得到target,说明顺序相同,只是插入了其他字母
string findLongestWord(string s, vector<string>& d) {
//定义变量储存最大值
string longestStr="";
//遍历容器
for(int i=0;i<d.size();i++){
//如果比当前最长已经匹配的单词还短,一定不是
if(longestStr.size()>d[i].size()){
continue;
}
//如果一样长顺序更大,一定不是,这里compare是比较了两个字符串的顺序大小
if(d[i].size()==longestStr.size() && longestStr.compare(d[i])<0){
continue;
}
//如果是子序列,储存最大
if(SubString(s,d[i])){
longestStr=d[i];
}
}
return longestStr;
}
//双指针判断是否是子序列
bool SubString(string s,string target){
int i=0,j=0;
while(i<s.length()&&j<target.length()){
//相同两个指针都后移一位
if(s[i]==target[j]){
j++;
}
//如果不相同,删除相当于s后移一位指针
i++;
}
if(j==target.length()){
return true;
}
return false;
}
};
二.二分查找
二分查找也称为折半查找,每次都能将查找区间减半,这种折半特性的算法时间复杂度为 O(logN)。 有序
正常实现代码
Input : [1,2,3,4,5]
key : 3
return the index : 2
public int binarySearch(int[] nums, int key) {
int l = 0, h = nums.length - 1;
while (l <= h) {
int m = l + (h - l) / 2;
if (nums[m] == key) {
return m;
} else if (nums[m] > key) {
h = m - 1;
} else {
l = m + 1;
}
}
return -1;
}
中值m的计算:有两种计算中值 m 的方式:
m = (l + h) / 2
m = l + (h - l) / 2
l + h 可能出现加法溢出,也就是说加法的结果大于整型能够表示的范围。但是 l 和 h 都为正数,因此 h - l 不会出现加法溢出问题。所以,最好使用第二种计算法方法。
未成功查找的返回值
循环退出时如果仍然没有查找到 key,那么表示查找失败。可以有两种返回值:
(1)-1:以一个错误码表示没有查找到 key
(2) l:将 key 插入到 nums 中的正确位置
变种
二分查找可以有很多变种,实现变种要注意边界值的判断。例如在一个有重复元素的数组中查找 key 的最左位置的实现如下:
public int binarySearch(int[] nums, int key) {
int l = 0, h = nums.length - 1;
while (l < h) {
int m = l + (h - l) / 2;
if (nums[m] >= key) {
h = m;
} else {
l = m + 1;
}
}
return l;
}
该实现和正常实现有以下不同:
h 的赋值表达式为 h = m
循环条件为 l < h
最后返回 l 而不是 -1
在 nums[m] >= key 的情况下,可以推导出最左 key 位于 [l, m] 区间中,这是一个闭区间。h 的赋值表达式为 h = m,因为 m 位置也可能是解。
在 h 的赋值表达式为 h = m 的情况下,如果循环条件为 l <= h,那么会出现循环无法退出的情况,因此循环条件只能是 l < h。以下演示了循环条件为 l <= h 时循环无法退出的情况:
nums = {0, 1, 2}, key = 1
l m h
0 1 2 nums[m] >= key
0 0 1 nums[m] < key
1 1 1 nums[m] >= key
1 1 1 nums[m] >= key
…
当循环体退出时,不表示没有查找到 key,因此最后返回的结果不应该为 -1。为了验证有没有查找到,需要在调用端判断一下返回位置上的值和 key 是否相等。
8.x的平方根(easy 69)
实现 int sqrt(int x) 函数。
计算并返回 x 的平方根,其中 x 是非负整数。
由于返回类型是整数,结果只保留整数的部分,小数部分将被舍去。
思路1:
一个数 x 的开方 sqrt 一定在 0 ~ x 之间,并且满足 sqrt == x / sqrt。可以利用二分查找在 0 ~ x 之间查找 sqrt。
对于 x = 8,它的开方是 2.82842…,最后应该返回 2 而不是 3。在循环条件为 l <= h 并且循环退出时,h 总是比 l 小 1,也就是说 h = 2,l = 3,因此最后的返回值应该为 h 而不是 l。
class Solution {
public