Leetcode C++ 刷题记录 - 数组和字符串

1. 数组

1.1. 二分查找

  • STL中关于二分查找的函数有三个lower_bound、upper_bound、binary_search。这三个函数都运用于有序区间.

    • ForwardIter lower_bound(ForwardIter first, ForwardIter last, const _Tp &val)算法返回一个非递减序列[first, last)中的第一个大于等于值val的位置;
    • ForwardIter upper_bound(ForwardIter first, ForwardIter last, const _Tp& val)算法返回一个非递减序列[first, last)中的第一个大于值val的位置;
  • 相关题目:

    • #704 二分查找
    • #35 搜索插入位置
    • #34 在排序数组中查找元素的第一个和最后一个位置
    • #69 x x x 的平方根
    • #367 有效的完全平方数
    • #33 搜索旋转排序数组
    • #81 搜索旋转排序数组 II
    • #154 寻找旋转排序数组中的最小值 II(待完成)
    • #540 有序数组中的单一元素(待完成)
    • #4 寻找两个正序数组的中位数(待完成)

1.1.1. #704 二分查找

// 左闭右闭写法
int binSearch(vector<int> &nums, int target)
{
  int l = 0, r = nums.size() - 1;
  while (l <= r)
  {
    int mid = (l + r) / 2;
    if (nums[mid] > target)
      r = mid - 1;
    else if (nums[mid] < target)
      l = mid + 1;
    else
      return mid;
  }
  return -1;
}
// 左闭右开写法
int binSearch(vector<int> &nums, int target)
{
  int l = 0, r = nums.size();
  while (left < right)
  {
    int mid = (l + r) / 2;
    if (nums[mid] > target)
      r = mid;
    else if (nums[mid] < target)
      l = mid + 1;
    else
      return mid;
  }
  return -1;
}

1.1.2. #35 搜索插入位置

给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。你可以假设数组中无重复元素。

// 暴力解法
int searchInsert(vector<int> &nums, int target)
{
  for (int i = 0; i < nums.size(); i++)
  {
    // 处理如下三种情况:目标值在数组所有元素之前;等于数组中某一个元素;插入数组中的位置
    if (nums[i] >= target)
      return i;
    }
  }
  // 目标值在数组所有元素之后的情况
  return nums.size();
}
// 二分法
int searchInsert(vector<int> &nums, int target)
{
  int n = nums.size();
  // 插入位置为最左边
  if (n < 1 || target < nums[0])
    return 0;

  // 插入位置在最右边
  if (target > nums[n - 1])
    return n;

  // 目标值存在数组内
  int l = 0, r = n - 1;
  while (l <= r)
  {
    int mid = (l + r) / 2;
    if (nums[mid] == target)
      return mid;
    else if (nums[mid] > target)
      r = mid - 1;
    else
      l = mid + 1;
  }

  // 插入位置为数组内部
  return r + 1;
}

1.1.3. #34 在排序数组中查找元素的第一个和最后一个位置

给定一个按照升序排列的整数数组nums,和一个目标值target。找出给定目标值在数组中的开始位置和结束位置。如果数组中不存在目标值target,返回[-1, -1]
进阶:你可以设计并实现时间复杂度为 O ( l o g n ) O(logn) O(logn)的算法解决此问题吗?

// 暴力解法
vector<int> searchRange(vector<int> &nums, int target)
{
  vector<int> ret = {-1, -1};
  int n = nums.size();
  if (n < 1)
    return ret;

  for (int i = 0; i < n; ++i)
  {
    if (nums[i] == target)
    {
      ret[0] = ret[1] = i;
      int j = i + 1;
      while (j < n && nums[j] == target)
        ++j;
      ret[1] = j - 1;
      break;
    }
  }

  return ret;
}
// 二分查找
vector<int> searchRange(vector<int> &nums, int target)
{
  if (nums.empty())
    return vector<int>{-1, -1};

  int lower = lower_bound(nums, target);
  int upper = upper_bound(nums, target) - 1; // 这里需要减1位

  if (lower == nums.size() || nums[lower] != target)
    return vector<int>{-1, -1};
  return vector<int>{lower, upper};
}

// 辅函数
int lower_bound(vector<int> &nums, int target)
{
  int l = 0, r = nums.size() - 1, mid;
  while (l <= r)
  {
    mid = (l + r) / 2;
    if (nums[mid] >= target)
      r = mid - 1;
    else
      l = mid + 1;
  }
  return l;
}

// 辅函数
int upper_bound(vector<int> &nums, int target)
{
  int l = 0, r = nums.size() - 1, mid;
  while (l <= r)
  {
    mid = (l + r) / 2;
    if (nums[mid] > target)
      r = mid - 1;
    else
      l = mid + 1;
  }
  return l;
}

1.1.4. #69 x 的平方根

实现int sqrt(int x)函数。计算并返回x的平方根,其中x是非负整数。由于返回类型是整数,结果只保留整数的部分,小数部分将被舍去。

// 二分查找(左闭右闭)思想
int mySqrt(int x)
{
  if (x == 0)
    return 0;

  long long l = 1, r = x, mid = 0;
  while (l <= r)
  {
    mid = (l + r) / 2;
    if (mid * mid < x)
      l = mid + 1;
    else if (mid * mid > x)
      r = mid - 1;
    else
      return mid;
  }
  return r;
}

// 牛顿迭代法???

1.1.5. #367 有效的完全平方数

bool isPerfectSquare(int num)
{
  if (num == 0)
    return true;

  long long l = 1, r = num, mid = 0;
  while (l <= r)
  {
    mid = (l + r) / 2;
    if (mid * mid > num)
      r = mid - 1;
    else if (mid * mid < num)
      l = mid + 1;
    else
      return true;
  }
  return false;
}

1.1.6. #33 搜索旋转排序数组

整数数组nums按升序排列,数组中的值互不相同。在传递给函数之前,nums在预先未知的某个下标k(0 <= k < nums.length)上进行了旋转,使数组变为[nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]](下标从0开始计数)。给你旋转后的数组nums和一个整数target,如果nums中存在这个目标值target,则返回它的下标,否则返回-1

int search(vector<int> &nums, int target)
{
  int n = nums.size();
  int l = 0, r = n - 1, mid = 0;

  while (l <= r)
  {
    mid = (l + r) / 2;
    if (nums[mid] == target) return mid;
    if (nums[mid] >= nums[l]) // 数组左边有序
    {
      if (nums[mid] > target && nums[l] <= target) // 目标值是否在左边有序数组内?
        r = mid - 1;
      else
        l = mid + 1;
    }
    else if (nums[mid] <= nums[r]) // 数组右边有序
    {
      if (nums[mid] < target && nums[r] >= target) // 目标值是否在右边有序数组内?
        l = mid + 1;
      else
        r = mid - 1;
    }
  }
  return -1;
}

1.1.7. #81 搜索旋转排序数组 II

相对于#33,数组中的存在重复的值

// 时间复杂度:O(n),最坏情况下数组元素均相等且不为target,我们需要访问所有位置才能得出果。
// 空间复杂度:O(1)。
bool search(vector<int> &nums, int target)
{
  int n = nums.size();
  int l = 0, r = n - 1, mid = 0;

  while (l <= r)
  {
    mid = (l + r) / 2;
    if (nums[mid] == target)
      return true;

    if (nums[mid] < nums[r]) // 右区间有序
    {
      if (nums[mid] < target && nums[r] >= target)
        l = mid + 1;
      else
        r = mid - 1;
    }
    else if (nums[mid] > nums[l]) // 左区间有序
    {
      if (nums[mid] > target && nums[l] <= target)
        r = mid - 1;
      else
        l = mid + 1;
    }
    else if (nums[mid] == nums[l]) // 无法区分左区间是否有序
      ++l;
    else if (nums[mid] == nums[r]) // 无法区分右区间是否有序
      --r;
  }
  return false;
}

1.1.8. #154 寻找旋转排序数组中的最小值 II


1.1.9. #540 有序数组中的单一元素


1.1.10. #4 寻找两个正序数组的中位数


1.2. 双指针

  • 双指针主要用于遍历数组,两个指针指向不同的元素,从而协同完成任务。也可以延伸到多个数组的多个指针。
  • 若两个指针指向同一数组,遍历方向相同且不会相交,则也称为滑动窗口(两个指针包围的区域即为当前的窗口),经常用于区间搜索。
  • 若两个指针指向同一数组,但是遍历方向相反,则可以用来进行搜索,待搜索的数组往往是排好序的。
  • 相关题目:
    • #15 三数之和
    • #18 四数之和
    • #27 移除元素
    • #26 删除排序数组中的重复项
    • #283 移动零
    • #844 比较含退格的字符串
    • #977 有序数组的平方
    • #167 两数之和 II - 输入有序数组
    • #88 合并两个有序数组
    • #142 环形链表 II

1.2.1. #15 三数之和

// 双指针法,注意去除重复元组
vector<vector<int>> threeSum(vector<int> &nums)
{
  vector<vector<int>> ans;
  if (nums.size() < 3) return ans;

  sort(nums.begin(), nums.end());
  for (int i = 0; i < nums.size() - 2; ++i)
  {
    if (nums[i] > 0) return ans; // 排序后若首元素大于0,则不存在三数之和大于0
    if (i > 0 && nums[i] == nums[i - 1]) continue; // 去除重复的元组

    int left = i + 1, right = nums.size() - 1;
    while (left < right)
    {
      if (nums[i] + nums[left] + nums[right] < 0) ++left;
      else if (nums[i] + nums[left] + nums[right] > 0) --right;
      else
      {
        ans.push_back({nums[i], nums[left], nums[right]});
        while (left < right && nums[left + 1] == nums[left]) ++left;
        while (left < right && nums[right - 1] == nums[right]) --right;
        ++left; --right;
      }
    }
  }

  return ans;
}

1.2.2. #18 四数之和

vector<vector<int>> fourSum(vector<int> &nums, int target)
{
  vector<vector<int>> ans;
  if (nums.size() < 4) return ans;

  sort(nums.begin(), nums.end());
  for (int i = 0; i < nums.size() - 3; ++i)
  {
    if (i > 0 && nums[i] == nums[i - 1]) continue;

    for (int j = i + 1; j < nums.size() - 2; ++j)
    {
      if (j > i + 1 && nums[j] == nums[j - 1]) continue;

      int left = j + 1, right = nums.size() - 1;
      while (left < right)
      {
        if (nums[i] + nums[j] + nums[left] + nums[right] < target) ++left;
        else if (nums[i] + nums[j] + nums[left] + nums[right] > target) --right;
        else
        {
          ans.push_back({nums[i], nums[j], nums[left], nums[right]});
          while (left < right && nums[left + 1] == nums[left]) ++left;
          while (left < right && nums[right - 1] == nums[right]) --right;
          ++left; --right;
        }
      }
    }
  }
  return ans;
}

1.2.3. #27 移除元素

// 快慢指针,通过fast指针找到下一个不等于val的元素放到slow位置
int removeElement(vector<int> &nums, int val)
{
  int fast = 0, slow = 0;
  while (fast < nums.size())
  {
    if (val == nums[fast])
      fast++;
    else
      nums[slow++] = nums[fast++];
  }
  return slow;
}

1.2.4. #26 删除排序数组中的重复项

// 通过fast指针找到第一个不重复的元素放到slow位置
int removeDuplicates(vector<int> &nums)
{
  if (nums.size() <= 1)
    return nums.size();

  int fast = 1, slow = 1;
  while (fast < nums.size())
  {
    if (nums[fast] == nums[fast - 1])
      fast++;
    else
      nums[slow++] = nums[fast++];
  }
  return slow;
}

1.2.5. #283 移动零

void moveZeroes(vector<int> &nums)
{
  int fast = 0, slow = 0;
  while (fast < nums.size())
  {
    if (nums[fast] == 0)
      fast++;
    else
      nums[slow++] = nums[fast++];
  }

  for (int i = slow; i < nums.size(); ++i)
    nums[i] = 0;
}

1.2.6. #844 比较含退格的字符串

// 从后往前比,某个字符是否被删除掉,只与其前面的'#'有关
// string S = "bxj##tw", T = "bxo#j##tw"
bool backspaceCompare(string S, string T)
{
  int i = S.size() - 1, j = T.size() - 1;

  while (i >= 0 || j >= 0)
  {
    int delSNum = 0, delTNum = 0;

    // 找到S中第一个要比较的字符
    while (i >= 0)
    {
      if (S[i] == '#')
      {
        ++delSNum;
        --i;
      }
      else if (delSNum > 0)
      {
        --delSNum;
        --i;
      }
      else
        break;
    }

    // 找到T中第一个要比较的字符
    while (j >= 0)
    {
      if (T[j] == '#')
      {
        ++delTNum;
        --j;
      }
      else if (delTNum > 0)
      {
        --delTNum;
        --j;
      }
      else
        break;
    }

    if (i < 0 && j < 0)
      return true;
    if ((i < 0 && j >= 0) || (i >= 0 && j < 0))
      return false;

    if (S[i] != T[j])
      return false;
    else
    {
      --i;
      --j;
    }
  }
  return true;
}

1.2.7. #977 有序数组的平方

vector<int> sortedSquares(vector<int> &nums)
{
  vector<int> ret(nums.size(), 0);
  int pos = nums.size() - 1;
  for (int i = 0, j = nums.size() - 1; i <= j;)
  {
    if (nums[i] * nums[i] < nums[j] * nums[j])
    {
      ret[pos--] = nums[j] * nums[j];
      j--;
    }
    else
    {
      ret[pos--] = nums[i] * nums[i];
      i++;
    }
  }
  return ret;
}

1.2.8. #167 两数之和 II - 输入有序数组

vector<int> twoSum(vector<int> &numbers, int target)
{
  vector<int> ret(2, 0);
  int l = 0, r = numbers.size() - 1;

  while (l < r)
  {
    if (numbers[l] + numbers[r] == target)
    {
      ret[0] = l + 1;
      ret[1] = r + 1;
      return ret;
    }
    if (numbers[l] + numbers[r] < target) // 两数和小于目标值,左指针右移一位
      ++l;
    else // 两数和大于目标值,右指针左移一位
      --r;
  }
  return ret;
}

1.2.9. #88 合并两个有序数组

void merge(vector<int> &nums1, int m, vector<int> &nums2, int n)
{
  int p1 = m - 1, p2 = n - 1, p3 = m + n - 1;
  while (p2 >= 0)
  {
    if (p1 < 0)
    {
      for (int i = 0; i <= p2; ++i)
        nums1[i] = nums2[i];
      break;
    }

    if (nums1[p1] < nums2[p2])
      nums1[p3--] = nums2[p2--];
    else
      nums1[p3--] = nums1[p1--];
  }
}

1.2.10. #142 环形链表 II

对于链表找环路的问题,有一个通用的解法——快慢指针(Floyd判圈法)。给定两个指针,分别命名为 slowfast,起始位置在链表的开头。每次fast前进两步,slow前进一步。如果fast可以走到尽头,那么说明没有环路;如果fast可以无限走下去,那么说明一定有环路,且一定存在一个时刻 slowfast相遇。当slowfast第一次相遇时,我们将fast重新移动到链表开头,并让slowfast每次都前进一步。当slowfast第二次相遇时,相遇的节点即为环路的开始点。

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
ListNode *detectCycle(ListNode *head)
{
  ListNode *fast = head, *slow = head;
  
  do
  {
    if (!fast || !fast->next) // fast是否可以走到尽头?
      return nullptr;
    fast = fast->next->next; // fast走两步
    slow = slow->next; // slow走一步
  } while (fast != slow); // 当fast和slow首次相遇时代表存在环路 

  fast = head;
  while (fast != slow)
  {
    fast = fast->next;
    slow = slow->next;
  }
  return fast;
}

1.3. 滑动窗口

  • 相关题目
    • #209 长度最小的子数组
    • #904 水果成篮
    • #76 最小覆盖子串

1.3.1. #209 长度最小的子数组

// 暴力解法,以数组中的每个元素为起点开始
int minSubArrayLen(int target, vector<int> &nums)
{
  int minLen = nums.size() + 1;
  for (int i = 0; i < nums.size(); ++i)
  {
    int sum = 0, j = i;
    while (sum < target)
    {
      sum += nums[j++];
      if (j >= nums.size())
        break;
    }
    if (sum >= target && minLen > j - i)
      minLen = j - i;
  }

  if (minLen != nums.size() + 1)
    return minLen;
  else
    return 0;
}
// 滑动窗口解法
int minSubArrayLen(int target, vector<int> &nums)
{
  if (nums.size() == 0)
    return 0;

  int start = 0, end = 0, minLen = nums.size() + 1;
  int sum = 0;
  while (end < nums.size())
  {
    sum += nums[end];
    while (sum >= target)
    {
      minLen = min(minLen, end - start + 1);
      sum -= nums[start];
      ++start;
    }
    ++end;
  }
  return (minLen == nums.size() + 1) ? 0 : minLen;
}

1.3.2. #904 水果成篮

剥掉马甲该题本质就是求最多包含两个(k=2)不同字符的最大窗口大小。

int totalFruit(vector<int> &tree)
{
  unordered_map<int, int> window;
  const int k = 2;
  int start = 0, end = 0, maxLen = 0;

  while (end < tree.size())
  {
    window[tree[end]]++;
    // 当窗口内包含了多于k个不同的字符时,滑动窗口左端点直至窗口内只包含两种不同的字符
    while (window.size() > k)
    {
      window[tree[start]]--;
      if (window[tree[start]] == 0)
        window.erase(tree[start]);
      ++start;
    }
    maxLen = max(maxLen, end - start + 1);
    ++end;
  }
  return maxLen;
}

1.3.3. #76 最小覆盖子串

string minWindow(string S, string T)
{
  // 先统计T中的字符情况,同时need中记录了窗口内需要的字符情况
  unordered_map<char, int> need;
  for (int i = 0; i < T.size(); ++i)
    need[T[i]] = 0;
  for (int i = 0; i < T.size(); ++i)
    ++need[T[i]];

  int start = 0, end = 0, minStart = 0, minLen = S.size() + 1, needNum = T.size();
  while (end < S.size())
  {
    // 窗口内增加了一个需要的字符
    if (need.find(S[end]) != need.end())
    {
      // 将该字符所需数目减1,当该字符所需数目仍大于0时,将总的所需字符目减1
      if (--need[S[end]] >= 0)
        --needNum;

      // 若目前滑动窗口已包含T中全部字符,则尝试将l右移,在不影响结果的情况下获得最短子字符串
      while (needNum == 0)
      {
        if (minLen > end - start + 1) // 该窗口比原来满足需求的窗口小?
        {
          minLen = end - start + 1;
          minStart = start;
        }

        // 窗口端点左移可能会使得所需字符数变大
        if ((need.find(S[start]) != need.end()) && ++need[S[start]] > 0)
          ++needNum;
        ++start;
      }
    }
    ++end;
  }
  return (minLen == S.size() + 1) ? "" : S.substr(minStart, minLen);
}

2. 字符串

2.1. 引言

2.2. 相关题目

  • #344 反转字符串
  • #541 反转字符串II
  • #151 翻转字符串里的单词
  • #28 实现strStr()(KMP算法)
  • #459 重复的子字符串

2.2.1. #541 反转字符串II

string reverseStr(string s, int k)
{
  for (int i = 0; i < s.size(); i += 2 * k)
  {
    if (s.size() - i >= k)
      reverse(s, i, i + k - 1);
    else
      reverse(s, i, s.size() - 1);
  }
  return s;
}

void reverse(string &s, int left, int right)
{
  while (left < right)
  {
    swap(s[left], s[right]);
    ++left;
    --right;
  }
}

2.2.2. #151 翻转字符串里的单词

string reverseWords(string s)
{
  int n = s.size();
  int l = n - 1, r = n - 1;

  string ans;
  while (l >= 0)
  {
    // 跳过空格找到字母
    while (l >= 0 && s[l] == ' '){ --l; --r; }

    // 记录一个单词的范围:[l+1, r]
    while (l >= 0 && s[l] != ' ') --l;

    if (l != r) //代表找到一个单词
    {
      for (int i = l + 1; i <= r; ++i)
        ans.push_back(s[i]);
      ans.push_back(' ');
      r = l;
    }
  }

  // 后面会多加一个空格
  if (ans[ans.size() - 1] == ' ')
    ans.pop_back();

  return ans;
}
// 原址处理,空间复杂度为O(1)的方法
// 移除多余空格,将字符串整体反转,反转字符串内的每一个单词
string reverseWords(string s)
{
  int l = 0, r = 0, end = s.size() - 1;
  while (r < s.size() && s[r] == ' ') ++r; //去除起始的空格
  while (end >= 0 && s[end] == ' ') --end; //去除末尾空格

  while (r <= end) //使用快慢指针移除中间多余的空格
  {
    while (r <= end && s[r] != ' ') s[l++] = s[r++];
    if (r < end)
    {
      s[l++] = s[r++]; //单词之间需要保留一个空格
      while (r < s.size() && s[r] == ' ') ++r; //处理单词之间多余的空格
    }
  }
  s.resize(l);

  reverse(s.begin(), s.end()); //将字符串整体反转

  l = 0, r = 0;
  while (r < s.size())
  {
    while (r < s.size() && s[r] != ' ') ++r;
    reverse(s.begin() + l, s.begin() + r); //反转字符串中的每一个的单词
    ++r; //跳过空格
    l = r; //处理下一个单词
  }

  return s;
}

2.2.3. #28 实现strStr()

KMP算法思想:通过分析模式串的先验信息,使得当出现字符串不匹配时,可以只回退模式串,而不用回退源字符串。时间复杂度: O ( n + m ) O(n+m) O(n+m),至多需要遍历两字符串一次。

//对模式串进行先验分析
void getPi(vector<int> &pi, const string &s)
{
  int j = 0;
  pi[0] = 0;
  for (int i = 1; i < s.size(); ++i)
  {
    while (j > 0 && s[i] != s[j]) j = pi[j - 1];
    if (s[i] == s[j]) ++j;
    pi[i] = j;
  }
}

int strStr(string haystack, string needle)
{
  int n = haystack.size(), m = needle.size();
  if (m < 1) return 0;
  if (m > n) return -1;

  vector<int> pi(m, 0);
  getPi(pi, needle);

  int j = 0;
  for (int i = 0; i < n; ++i)
  {
    while (j > 0 && haystack[i] != needle[j]) j = pi[j - 1];
    if (haystack[i] == needle[j]) ++j;
    if (j == m) return i - m + 1;
  }
  return -1;
}

2.2.4. #459 重复的子字符串

bool repeatedSubstringPattern(string s)
{
  int n = s.size();
  if (n == 0) return false;

  vector<int> pi(n, 0);
  pi[0] = 0;
  int j = 0;
  for (int i = 1; i < n; ++i)
  {
    while (j > 0 && s[i] != s[j]) j = pi[j - 1];
    if (s[i] == s[j]) ++j;
    pi[i] = j;
  }

  if (pi[n - 1] != 0 && n % (n - pi[n - 1]) == 0)
    return true;
  return false;
}



1. 数组

1.1. 二分查找

  • STL中关于二分查找的函数有三个lower_bound、upper_bound、binary_search。这三个函数都运用于有序区间.

    • ForwardIter lower_bound(ForwardIter first, ForwardIter last, const _Tp &val)算法返回一个非递减序列[first, last)中的第一个大于等于值val的位置;
    • ForwardIter upper_bound(ForwardIter first, ForwardIter last, const _Tp& val)算法返回一个非递减序列[first, last)中的第一个大于值val的位置;
  • 相关题目:

    • #704 二分查找
    • #35 搜索插入位置
    • #34 在排序数组中查找元素的第一个和最后一个位置
    • #69 x x x 的平方根
    • #367 有效的完全平方数
    • #33 搜索旋转排序数组
    • #81 搜索旋转排序数组 II
    • #154 寻找旋转排序数组中的最小值 II(待完成)
    • #540 有序数组中的单一元素(待完成)
    • #4 寻找两个正序数组的中位数(待完成)

1.1.1. #704 二分查找

// 左闭右闭写法
int binSearch(vector<int> &nums, int target)
{
  int l = 0, r = nums.size() - 1;
  while (l <= r)
  {
    int mid = (l + r) / 2;
    if (nums[mid] > target)
      r = mid - 1;
    else if (nums[mid] < target)
      l = mid + 1;
    else
      return mid;
  }
  return -1;
}
// 左闭右开写法
int binSearch(vector<int> &nums, int target)
{
  int l = 0, r = nums.size();
  while (left < right)
  {
    int mid = (l + r) / 2;
    if (nums[mid] > target)
      r = mid;
    else if (nums[mid] < target)
      l = mid + 1;
    else
      return mid;
  }
  return -1;
}

1.1.2. #35 搜索插入位置

给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。你可以假设数组中无重复元素。

// 暴力解法
int searchInsert(vector<int> &nums, int target)
{
  for (int i = 0; i < nums.size(); i++)
  {
    // 处理如下三种情况:目标值在数组所有元素之前;等于数组中某一个元素;插入数组中的位置
    if (nums[i] >= target)
      return i;
    }
  }
  // 目标值在数组所有元素之后的情况
  return nums.size();
}
// 二分法
int searchInsert(vector<int> &nums, int target)
{
  int n = nums.size();
  // 插入位置为最左边
  if (n < 1 || target < nums[0])
    return 0;

  // 插入位置在最右边
  if (target > nums[n - 1])
    return n;

  // 目标值存在数组内
  int l = 0, r = n - 1;
  while (l <= r)
  {
    int mid = (l + r) / 2;
    if (nums[mid] == target)
      return mid;
    else if (nums[mid] > target)
      r = mid - 1;
    else
      l = mid + 1;
  }

  // 插入位置为数组内部
  return r + 1;
}

1.1.3. #34 在排序数组中查找元素的第一个和最后一个位置

给定一个按照升序排列的整数数组nums,和一个目标值target。找出给定目标值在数组中的开始位置和结束位置。如果数组中不存在目标值target,返回[-1, -1]
进阶:你可以设计并实现时间复杂度为 O ( l o g n ) O(logn) O(logn)的算法解决此问题吗?

// 暴力解法
vector<int> searchRange(vector<int> &nums, int target)
{
  vector<int> ret = {-1, -1};
  int n = nums.size();
  if (n < 1)
    return ret;

  for (int i = 0; i < n; ++i)
  {
    if (nums[i] == target)
    {
      ret[0] = ret[1] = i;
      int j = i + 1;
      while (j < n && nums[j] == target)
        ++j;
      ret[1] = j - 1;
      break;
    }
  }

  return ret;
}
// 二分查找
vector<int> searchRange(vector<int> &nums, int target)
{
  if (nums.empty())
    return vector<int>{-1, -1};

  int lower = lower_bound(nums, target);
  int upper = upper_bound(nums, target) - 1; // 这里需要减1位

  if (lower == nums.size() || nums[lower] != target)
    return vector<int>{-1, -1};
  return vector<int>{lower, upper};
}

// 辅函数
int lower_bound(vector<int> &nums, int target)
{
  int l = 0, r = nums.size() - 1, mid;
  while (l <= r)
  {
    mid = (l + r) / 2;
    if (nums[mid] >= target)
      r = mid - 1;
    else
      l = mid + 1;
  }
  return l;
}

// 辅函数
int upper_bound(vector<int> &nums, int target)
{
  int l = 0, r = nums.size() - 1, mid;
  while (l <= r)
  {
    mid = (l + r) / 2;
    if (nums[mid] > target)
      r = mid - 1;
    else
      l = mid + 1;
  }
  return l;
}

1.1.4. #69 x 的平方根

实现int sqrt(int x)函数。计算并返回x的平方根,其中x是非负整数。由于返回类型是整数,结果只保留整数的部分,小数部分将被舍去。

// 二分查找(左闭右闭)思想
int mySqrt(int x)
{
  if (x == 0)
    return 0;

  long long l = 1, r = x, mid = 0;
  while (l <= r)
  {
    mid = (l + r) / 2;
    if (mid * mid < x)
      l = mid + 1;
    else if (mid * mid > x)
      r = mid - 1;
    else
      return mid;
  }
  return r;
}

// 牛顿迭代法???

1.1.5. #367 有效的完全平方数

bool isPerfectSquare(int num)
{
  if (num == 0)
    return true;

  long long l = 1, r = num, mid = 0;
  while (l <= r)
  {
    mid = (l + r) / 2;
    if (mid * mid > num)
      r = mid - 1;
    else if (mid * mid < num)
      l = mid + 1;
    else
      return true;
  }
  return false;
}

1.1.6. #33 搜索旋转排序数组

整数数组nums按升序排列,数组中的值互不相同。在传递给函数之前,nums在预先未知的某个下标k(0 <= k < nums.length)上进行了旋转,使数组变为[nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]](下标从0开始计数)。给你旋转后的数组nums和一个整数target,如果nums中存在这个目标值target,则返回它的下标,否则返回-1

int search(vector<int> &nums, int target)
{
  int n = nums.size();
  int l = 0, r = n - 1, mid = 0;

  while (l <= r)
  {
    mid = (l + r) / 2;
    if (nums[mid] == target) return mid;
    if (nums[mid] >= nums[l]) // 数组左边有序
    {
      if (nums[mid] > target && nums[l] <= target) // 目标值是否在左边有序数组内?
        r = mid - 1;
      else
        l = mid + 1;
    }
    else if (nums[mid] <= nums[r]) // 数组右边有序
    {
      if (nums[mid] < target && nums[r] >= target) // 目标值是否在右边有序数组内?
        l = mid + 1;
      else
        r = mid - 1;
    }
  }
  return -1;
}

1.1.7. #81 搜索旋转排序数组 II

相对于#33,数组中的存在重复的值

// 时间复杂度:O(n),最坏情况下数组元素均相等且不为target,我们需要访问所有位置才能得出果。
// 空间复杂度:O(1)。
bool search(vector<int> &nums, int target)
{
  int n = nums.size();
  int l = 0, r = n - 1, mid = 0;

  while (l <= r)
  {
    mid = (l + r) / 2;
    if (nums[mid] == target)
      return true;

    if (nums[mid] < nums[r]) // 右区间有序
    {
      if (nums[mid] < target && nums[r] >= target)
        l = mid + 1;
      else
        r = mid - 1;
    }
    else if (nums[mid] > nums[l]) // 左区间有序
    {
      if (nums[mid] > target && nums[l] <= target)
        r = mid - 1;
      else
        l = mid + 1;
    }
    else if (nums[mid] == nums[l]) // 无法区分左区间是否有序
      ++l;
    else if (nums[mid] == nums[r]) // 无法区分右区间是否有序
      --r;
  }
  return false;
}

1.1.8. #154 寻找旋转排序数组中的最小值 II


1.1.9. #540 有序数组中的单一元素


1.1.10. #4 寻找两个正序数组的中位数


1.2. 双指针

  • 双指针主要用于遍历数组,两个指针指向不同的元素,从而协同完成任务。也可以延伸到多个数组的多个指针。
  • 若两个指针指向同一数组,遍历方向相同且不会相交,则也称为滑动窗口(两个指针包围的区域即为当前的窗口),经常用于区间搜索。
  • 若两个指针指向同一数组,但是遍历方向相反,则可以用来进行搜索,待搜索的数组往往是排好序的。
  • 相关题目:
    • #15 三数之和
    • #18 四数之和
    • #27 移除元素
    • #26 删除排序数组中的重复项
    • #283 移动零
    • #844 比较含退格的字符串
    • #977 有序数组的平方
    • #167 两数之和 II - 输入有序数组
    • #88 合并两个有序数组
    • #142 环形链表 II

1.2.1. #15 三数之和

// 双指针法,注意去除重复元组
vector<vector<int>> threeSum(vector<int> &nums)
{
  vector<vector<int>> ans;
  if (nums.size() < 3) return ans;

  sort(nums.begin(), nums.end());
  for (int i = 0; i < nums.size() - 2; ++i)
  {
    if (nums[i] > 0) return ans; // 排序后若首元素大于0,则不存在三数之和大于0
    if (i > 0 && nums[i] == nums[i - 1]) continue; // 去除重复的元组

    int left = i + 1, right = nums.size() - 1;
    while (left < right)
    {
      if (nums[i] + nums[left] + nums[right] < 0) ++left;
      else if (nums[i] + nums[left] + nums[right] > 0) --right;
      else
      {
        ans.push_back({nums[i], nums[left], nums[right]});
        while (left < right && nums[left + 1] == nums[left]) ++left;
        while (left < right && nums[right - 1] == nums[right]) --right;
        ++left; --right;
      }
    }
  }

  return ans;
}

1.2.2. #18 四数之和

vector<vector<int>> fourSum(vector<int> &nums, int target)
{
  vector<vector<int>> ans;
  if (nums.size() < 4) return ans;

  sort(nums.begin(), nums.end());
  for (int i = 0; i < nums.size() - 3; ++i)
  {
    if (i > 0 && nums[i] == nums[i - 1]) continue;

    for (int j = i + 1; j < nums.size() - 2; ++j)
    {
      if (j > i + 1 && nums[j] == nums[j - 1]) continue;

      int left = j + 1, right = nums.size() - 1;
      while (left < right)
      {
        if (nums[i] + nums[j] + nums[left] + nums[right] < target) ++left;
        else if (nums[i] + nums[j] + nums[left] + nums[right] > target) --right;
        else
        {
          ans.push_back({nums[i], nums[j], nums[left], nums[right]});
          while (left < right && nums[left + 1] == nums[left]) ++left;
          while (left < right && nums[right - 1] == nums[right]) --right;
          ++left; --right;
        }
      }
    }
  }
  return ans;
}

1.2.3. #27 移除元素

// 快慢指针,通过fast指针找到下一个不等于val的元素放到slow位置
int removeElement(vector<int> &nums, int val)
{
  int fast = 0, slow = 0;
  while (fast < nums.size())
  {
    if (val == nums[fast])
      fast++;
    else
      nums[slow++] = nums[fast++];
  }
  return slow;
}

1.2.4. #26 删除排序数组中的重复项

// 通过fast指针找到第一个不重复的元素放到slow位置
int removeDuplicates(vector<int> &nums)
{
  if (nums.size() <= 1)
    return nums.size();

  int fast = 1, slow = 1;
  while (fast < nums.size())
  {
    if (nums[fast] == nums[fast - 1])
      fast++;
    else
      nums[slow++] = nums[fast++];
  }
  return slow;
}

1.2.5. #283 移动零

void moveZeroes(vector<int> &nums)
{
  int fast = 0, slow = 0;
  while (fast < nums.size())
  {
    if (nums[fast] == 0)
      fast++;
    else
      nums[slow++] = nums[fast++];
  }

  for (int i = slow; i < nums.size(); ++i)
    nums[i] = 0;
}

1.2.6. #844 比较含退格的字符串

// 从后往前比,某个字符是否被删除掉,只与其前面的'#'有关
// string S = "bxj##tw", T = "bxo#j##tw"
bool backspaceCompare(string S, string T)
{
  int i = S.size() - 1, j = T.size() - 1;

  while (i >= 0 || j >= 0)
  {
    int delSNum = 0, delTNum = 0;

    // 找到S中第一个要比较的字符
    while (i >= 0)
    {
      if (S[i] == '#')
      {
        ++delSNum;
        --i;
      }
      else if (delSNum > 0)
      {
        --delSNum;
        --i;
      }
      else
        break;
    }

    // 找到T中第一个要比较的字符
    while (j >= 0)
    {
      if (T[j] == '#')
      {
        ++delTNum;
        --j;
      }
      else if (delTNum > 0)
      {
        --delTNum;
        --j;
      }
      else
        break;
    }

    if (i < 0 && j < 0)
      return true;
    if ((i < 0 && j >= 0) || (i >= 0 && j < 0))
      return false;

    if (S[i] != T[j])
      return false;
    else
    {
      --i;
      --j;
    }
  }
  return true;
}

1.2.7. #977 有序数组的平方

vector<int> sortedSquares(vector<int> &nums)
{
  vector<int> ret(nums.size(), 0);
  int pos = nums.size() - 1;
  for (int i = 0, j = nums.size() - 1; i <= j;)
  {
    if (nums[i] * nums[i] < nums[j] * nums[j])
    {
      ret[pos--] = nums[j] * nums[j];
      j--;
    }
    else
    {
      ret[pos--] = nums[i] * nums[i];
      i++;
    }
  }
  return ret;
}

1.2.8. #167 两数之和 II - 输入有序数组

vector<int> twoSum(vector<int> &numbers, int target)
{
  vector<int> ret(2, 0);
  int l = 0, r = numbers.size() - 1;

  while (l < r)
  {
    if (numbers[l] + numbers[r] == target)
    {
      ret[0] = l + 1;
      ret[1] = r + 1;
      return ret;
    }
    if (numbers[l] + numbers[r] < target) // 两数和小于目标值,左指针右移一位
      ++l;
    else // 两数和大于目标值,右指针左移一位
      --r;
  }
  return ret;
}

1.2.9. #88 合并两个有序数组

void merge(vector<int> &nums1, int m, vector<int> &nums2, int n)
{
  int p1 = m - 1, p2 = n - 1, p3 = m + n - 1;
  while (p2 >= 0)
  {
    if (p1 < 0)
    {
      for (int i = 0; i <= p2; ++i)
        nums1[i] = nums2[i];
      break;
    }

    if (nums1[p1] < nums2[p2])
      nums1[p3--] = nums2[p2--];
    else
      nums1[p3--] = nums1[p1--];
  }
}

1.2.10. #142 环形链表 II

对于链表找环路的问题,有一个通用的解法——快慢指针(Floyd判圈法)。给定两个指针,分别命名为 slowfast,起始位置在链表的开头。每次fast前进两步,slow前进一步。如果fast可以走到尽头,那么说明没有环路;如果fast可以无限走下去,那么说明一定有环路,且一定存在一个时刻 slowfast相遇。当slowfast第一次相遇时,我们将fast重新移动到链表开头,并让slowfast每次都前进一步。当slowfast第二次相遇时,相遇的节点即为环路的开始点。

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
ListNode *detectCycle(ListNode *head)
{
  ListNode *fast = head, *slow = head;
  
  do
  {
    if (!fast || !fast->next) // fast是否可以走到尽头?
      return nullptr;
    fast = fast->next->next; // fast走两步
    slow = slow->next; // slow走一步
  } while (fast != slow); // 当fast和slow首次相遇时代表存在环路 

  fast = head;
  while (fast != slow)
  {
    fast = fast->next;
    slow = slow->next;
  }
  return fast;
}

1.3. 滑动窗口

  • 相关题目
    • #209 长度最小的子数组
    • #904 水果成篮
    • #76 最小覆盖子串

1.3.1. #209 长度最小的子数组

// 暴力解法,以数组中的每个元素为起点开始
int minSubArrayLen(int target, vector<int> &nums)
{
  int minLen = nums.size() + 1;
  for (int i = 0; i < nums.size(); ++i)
  {
    int sum = 0, j = i;
    while (sum < target)
    {
      sum += nums[j++];
      if (j >= nums.size())
        break;
    }
    if (sum >= target && minLen > j - i)
      minLen = j - i;
  }

  if (minLen != nums.size() + 1)
    return minLen;
  else
    return 0;
}
// 滑动窗口解法
int minSubArrayLen(int target, vector<int> &nums)
{
  if (nums.size() == 0)
    return 0;

  int start = 0, end = 0, minLen = nums.size() + 1;
  int sum = 0;
  while (end < nums.size())
  {
    sum += nums[end];
    while (sum >= target)
    {
      minLen = min(minLen, end - start + 1);
      sum -= nums[start];
      ++start;
    }
    ++end;
  }
  return (minLen == nums.size() + 1) ? 0 : minLen;
}

1.3.2. #904 水果成篮

剥掉马甲该题本质就是求最多包含两个(k=2)不同字符的最大窗口大小。

int totalFruit(vector<int> &tree)
{
  unordered_map<int, int> window;
  const int k = 2;
  int start = 0, end = 0, maxLen = 0;

  while (end < tree.size())
  {
    window[tree[end]]++;
    // 当窗口内包含了多于k个不同的字符时,滑动窗口左端点直至窗口内只包含两种不同的字符
    while (window.size() > k)
    {
      window[tree[start]]--;
      if (window[tree[start]] == 0)
        window.erase(tree[start]);
      ++start;
    }
    maxLen = max(maxLen, end - start + 1);
    ++end;
  }
  return maxLen;
}

1.3.3. #76 最小覆盖子串

string minWindow(string S, string T)
{
  // 先统计T中的字符情况,同时need中记录了窗口内需要的字符情况
  unordered_map<char, int> need;
  for (int i = 0; i < T.size(); ++i)
    need[T[i]] = 0;
  for (int i = 0; i < T.size(); ++i)
    ++need[T[i]];

  int start = 0, end = 0, minStart = 0, minLen = S.size() + 1, needNum = T.size();
  while (end < S.size())
  {
    // 窗口内增加了一个需要的字符
    if (need.find(S[end]) != need.end())
    {
      // 将该字符所需数目减1,当该字符所需数目仍大于0时,将总的所需字符目减1
      if (--need[S[end]] >= 0)
        --needNum;

      // 若目前滑动窗口已包含T中全部字符,则尝试将l右移,在不影响结果的情况下获得最短子字符串
      while (needNum == 0)
      {
        if (minLen > end - start + 1) // 该窗口比原来满足需求的窗口小?
        {
          minLen = end - start + 1;
          minStart = start;
        }

        // 窗口端点左移可能会使得所需字符数变大
        if ((need.find(S[start]) != need.end()) && ++need[S[start]] > 0)
          ++needNum;
        ++start;
      }
    }
    ++end;
  }
  return (minLen == S.size() + 1) ? "" : S.substr(minStart, minLen);
}

2. 字符串

2.1. 引言

2.2. 相关题目

  • #344 反转字符串
  • #541 反转字符串II
  • #151 翻转字符串里的单词
  • #28 实现strStr()(KMP算法)
  • #459 重复的子字符串

2.2.1. #541 反转字符串II

string reverseStr(string s, int k)
{
  for (int i = 0; i < s.size(); i += 2 * k)
  {
    if (s.size() - i >= k)
      reverse(s, i, i + k - 1);
    else
      reverse(s, i, s.size() - 1);
  }
  return s;
}

void reverse(string &s, int left, int right)
{
  while (left < right)
  {
    swap(s[left], s[right]);
    ++left;
    --right;
  }
}

2.2.2. #151 翻转字符串里的单词

string reverseWords(string s)
{
  int n = s.size();
  int l = n - 1, r = n - 1;

  string ans;
  while (l >= 0)
  {
    // 跳过空格找到字母
    while (l >= 0 && s[l] == ' '){ --l; --r; }

    // 记录一个单词的范围:[l+1, r]
    while (l >= 0 && s[l] != ' ') --l;

    if (l != r) //代表找到一个单词
    {
      for (int i = l + 1; i <= r; ++i)
        ans.push_back(s[i]);
      ans.push_back(' ');
      r = l;
    }
  }

  // 后面会多加一个空格
  if (ans[ans.size() - 1] == ' ')
    ans.pop_back();

  return ans;
}
// 原址处理,空间复杂度为O(1)的方法
// 移除多余空格,将字符串整体反转,反转字符串内的每一个单词
string reverseWords(string s)
{
  int l = 0, r = 0, end = s.size() - 1;
  while (r < s.size() && s[r] == ' ') ++r; //去除起始的空格
  while (end >= 0 && s[end] == ' ') --end; //去除末尾空格

  while (r <= end) //使用快慢指针移除中间多余的空格
  {
    while (r <= end && s[r] != ' ') s[l++] = s[r++];
    if (r < end)
    {
      s[l++] = s[r++]; //单词之间需要保留一个空格
      while (r < s.size() && s[r] == ' ') ++r; //处理单词之间多余的空格
    }
  }
  s.resize(l);

  reverse(s.begin(), s.end()); //将字符串整体反转

  l = 0, r = 0;
  while (r < s.size())
  {
    while (r < s.size() && s[r] != ' ') ++r;
    reverse(s.begin() + l, s.begin() + r); //反转字符串中的每一个的单词
    ++r; //跳过空格
    l = r; //处理下一个单词
  }

  return s;
}

2.2.3. #28 实现strStr()

KMP算法思想:通过分析模式串的先验信息,使得当出现字符串不匹配时,可以只回退模式串,而不用回退源字符串。时间复杂度: O ( n + m ) O(n+m) O(n+m),至多需要遍历两字符串一次。

//对模式串进行先验分析
void getPi(vector<int> &pi, const string &s)
{
  int j = 0;
  pi[0] = 0;
  for (int i = 1; i < s.size(); ++i)
  {
    while (j > 0 && s[i] != s[j]) j = pi[j - 1];
    if (s[i] == s[j]) ++j;
    pi[i] = j;
  }
}

int strStr(string haystack, string needle)
{
  int n = haystack.size(), m = needle.size();
  if (m < 1) return 0;
  if (m > n) return -1;

  vector<int> pi(m, 0);
  getPi(pi, needle);

  int j = 0;
  for (int i = 0; i < n; ++i)
  {
    while (j > 0 && haystack[i] != needle[j]) j = pi[j - 1];
    if (haystack[i] == needle[j]) ++j;
    if (j == m) return i - m + 1;
  }
  return -1;
}

2.2.4. #459 重复的子字符串

bool repeatedSubstringPattern(string s)
{
  int n = s.size();
  if (n == 0) return false;

  vector<int> pi(n, 0);
  pi[0] = 0;
  int j = 0;
  for (int i = 1; i < n; ++i)
  {
    while (j > 0 && s[i] != s[j]) j = pi[j - 1];
    if (s[i] == s[j]) ++j;
    pi[i] = j;
  }

  if (pi[n - 1] != 0 && n % (n - pi[n - 1]) == 0)
    return true;
  return false;
}



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值