算法基础 典型题 数组与字符串

算法基础练习总结入口:我的算法地图

一、基本概念

1、数组: 是内存中一段连续的存储空间,通过索引直接访问元素。它是所有数据结构中最基础的形式,其他很多数据结构都是基于数组构建的。典型适用场景:需要随机访问、知道元素数量或大致范围的情况。
2、字符串:字符串是字符的序列,本质上是一种特殊的数组(字符数组)。特殊终止符(C语言风格)以’\0’结尾。
最常见的算法数据结构,由于字符串本质也是数组,二者很多题目和做法都是一致的,我们放在一起讨论。

二、场景分析

1、有哪些题目类型。 数组和字符串,属于线性表,成员可以随机访问,为算法题中最基础的数据结构,衍生出的题目类型非常丰富。以下是其典型的算法题类型分类。注意许多题目会交叉融合多种类型。

序号题目类型核心特点典型例题常用算法模式
1子数组/子串最值问题求满足条件的最长/最短/最大和无重复字符的最长子串 (LC 3)、最大子数组和 (LC 53)滑动窗口、动态规划
2元素查找与匹配在数组/字符串中查找目标元素、判断两数之和 (LC 1)、实现 strStr() (LC 28)哈希表、双指针、字符串匹配
3元素操作与修改原地修改数组(增删改查)、调整元素移除元素 (LC 27)、移动零 (LC 283)双指针、原地替换
4区间求和与统计计算子数组/子矩阵的和、统计满足条件的子序列数量和为K的子数组 (LC 560)、区域和检索 (LC 304)前缀和、差分、哈希表
5区间修改与更新对连续区间进行批量增减操作,最后获取结果航班预订统计 (LC 1109)、拼车 (LC 1094)差分数组
6字符频率与变形基于字符出现次数解决问题,对字符串进行特定变形有效的字母异位词 (LC 242)、字符串的排列 (LC 567)哈希表、排序
7回文相关问题判断或构造回文串、回文子串,利用回文的对称性验证回文串 (LC 125)、最长回文子串 (LC 5)双指针、中心扩展法
8多指针组合问题利用3个或更多指针解决复杂匹配问题三数之和 (LC 15)、四数之和 (LC 18)排序+双指针、多指针遍历
9矩阵相关问题二维数组的遍历、搜索、旋转、转置等操作螺旋矩阵 (LC 54)、旋转图像 (LC 48)模拟法、分层遍历

2、总结涉及最用的算法模式。 从算法模式的角度来看,数组和字符串总结如下:

序号算法模式核心思想优势/为什么适用适用场景典型案例
1滑动窗口维护动态变化的子数组/子串窗口,通过调整左右边界优化计算利用连续性只需处理新增/移除元素连续子序列问题无重复字符的最长子串 (LC 3)、长度最小的子数组 (LC 209)
2双指针用两个指针(同向/反向)在数组上移动,通过策略性移动优化效率利用有序性或对称性快速缩小范围,O(n)时间复杂度,节省空间有序数组查找(两数之和)、原地修改(移除元素)两数之和 II (LC 167)、移除元素 (LC 27)
3前缀和预处理前缀和数组,将子数组和计算优化为 O(1)避免重复计算,将 O(n²) 优化为 O(n)频繁计算子数组和、子数组和等于目标值和为K的子数组 (LC 560)、二维区域和检索 (LC 304)
4差分构建差分数组,将区间修改从 O(n) 优化为 O(1)将区间操作转化为两点操作,大幅减少重复计算,适合批量区间修改频繁区间增减操作、区间更新与单点查询航班预订统计 (LC 1109)、拼车 (LC 1094)
5哈希表用哈希表存储元素信息(值/索引/次数),将查找优化为 O(1)利用 O(1) 查找特性降低时间复杂度,适合"边遍历边查询"场景元素存在性判断(去重)、记录元素位置/次数、字符频率问题两数之和 (LC 1)、有效的字母异位词 (LC 242)
6排序+双指针先排序再用双指针,利用有序性优化查找和去重排序后可通过双指针高效解决多数问题,便于跳过重复元素,避免结果集重复元素大小关系问题(三数之和)、需要去重的问题三数之和 (LC 15)、最接近的三数之和 (LC 16)
7字符串匹配针对子串查找问题的专用算法(KMP/Rabin-Karp 等)比通用方法更高效,可将复杂度降至 O(n+m),适合长字符串处理主串中查找模式串的位置实现 strStr() (LC 28)、重复的子字符串 (LC 459)

三、典型题目

1、基础练习题-《程序员面试经典》题目

1)判定字符是否唯一
实现一个算法,确定一个字符串 s 的所有字符是否全都不同。(加分:不要用额外空间)

    bool isUnique(string astr) {
        // 方法1:使用表记录字符是否出现,复杂度 时间O(n)/空间O(1)
        // 题目限制的输入为小写字母,最多26个,可用u32记录
        int bitMap = 0;
        for (int i = 0; i < astr.size(); i++) {
            if ((bitMap & (1 << (astr[i] - 'a'))) == 0) {
                bitMap |= 1 << (astr[i] - 'a');
            } else {
                return false;
            }
        }
        return true;
    }

思考:最先想到的就是用 额外的空间记录是否出现(题目限制小写字母,最多26个,可以用一个u32的位操作记录),遍历一次,复杂度 时间O(n)/空间O(1)。扩展:如果数据量大一些,如未限制字符内容,可以用boot map[256]。

2)品牌判定是否互为字符重复
给定两个由小写字母组成的字符串 s1 和 s2,请编写一个程序,确定其中一个字符串的字符重新排列后,能否变成另一个字符串。(题解:省)

思考:一种是统计每个string的各个字符数量,再对比结果。复杂度 时间O(n)/空间O(1);一种是先排序,再对比。

3)URL化
URL化。编写一种方法,将字符串中的空格全部替换为%20。假定该字符串尾部有足够的空间存放新增字符,并且知道字符串的“真实”长度。(加分:不要用额外控制)

    string replaceSpaces(string S, int length) {
        int new_pos = S.size() - 1;
        for (int old_pos = length - 1; old_pos >= 0; old_pos--) {
            if (S[old_pos] == ' ') {
                S[new_pos--] = '0';
                S[new_pos--] = '2';
                S[new_pos--] = '%';
            } else {
                S[new_pos--] = S[old_pos];
            }
        }
        return S.substr(new_pos + 1);
    }

4)回文排列
给定一个字符串,编写一个函数判定其是否为某个回文串的排列之一。回文串是指正反两个方向都一样的单词或短语。排列是指字母的重新排列。回文串不一定是字典当中的单词。

    bool canPermutePalindrome(string s) {
        // 回文排列要求:只能有一个或零个,奇数字符存在;复杂度 时间O(n)/空间O(1)
        // 最小记录空间,由于只需要判断奇偶,可以用bool
        bool oddmap[256] = {0};
        for (int i = 0; i < s.size(); i++) {
            oddmap[s[i]] = !oddmap[s[i]];
        }
        int oddcnt = 0;
        for (int j = 0; j < 256; j++) {
            if (oddmap[j] == true) {
                oddcnt++;
            }
        }
        return oddcnt <= 1 ? true : false;
    }

5)一次编辑
字符串有三种编辑操作:插入一个英文字符、删除一个英文字符或者替换一个英文字符。 给定两个字符串,编写一个函数判定它们是否只需要一次(或者零次)编辑。

  bool oneEditAway(string first, string second) {
        int lenA = first.size();
        int lenB = second.size();
        // 长度差别大于1必然不可能系一次处理
        if (abs(lenA - lenB) > 1) {
            return false;
        }
        bool EditFlag = false;
        // 如果两个相等,只能最多有一个字符不同,两个一起跳过继续遍历
        // 如果零个不等,那必然是短的那个少一个字符,跳过一个
        int i = 0, j = 0;
        for (;i < lenA && j < lenB; i++,j++) {
            if (first[i] != second[j]) {
                if (EditFlag == false) {
                    EditFlag = true;
                    if (lenA > lenB) {
                        j--; // first长,second回退位置
                    } else if (lenA < lenB) {
                        i--; // second长,first回退位置
                    } else {
                        // 长度相同 一起跳
                    }
                } else {
                    return false;
                }
            }
        }
        return true;
    }

思考:双指针的一种,两个序列往前走,当遇到首个不同的时候,根据条件调整指针位置;

6)字符串压缩
字符串压缩。利用字符重复出现的次数,编写一种方法,实现基本的字符串压缩功能。比如,字符串aabcccccaaa会变为a2b1c5a3。若“压缩”后的字符串没有变短,则返回原先的字符串。你可以假设字符串中只包含大小写英文字母(a至z)。

     string compressString(string S) {
        int len = S.size();
        if (len == 0) {
            return S;
        }
        string cmpStr;
        int i = 0;
        while (i < len) {
            int j = i + 1;
            while (j < len && S[j] == S[i]) {
                j++;
            }
            cmpStr += S[i] + to_string(j - i);
            i = j;
        }
        return cmpStr.size() < len ? cmpStr : S;
    }

7) 旋转矩阵
给你一幅由 N × N 矩阵表示的图像,其中每个像素的大小为 4 字节。请你设计一种算法,将图像旋转 90 度。不占用额外内存空间能否做到?

class Solution {
public:
    void rotate(vector<vector<int>>& matrix) {
        int n = matrix.size();
        // 从外圈向内圈旋转找坐标交换规律
        for (int i = 0; i < n / 2; ++i) {
            for (int j = 0; j < (n + 1) / 2; ++j) {
                int temp = matrix[i][j];
                matrix[i][j] = matrix[n - j - 1][i];
                matrix[n - j - 1][i] = matrix[n - i - 1][n - j - 1];
                matrix[n - i - 1][n - j - 1] = matrix[j][n - i - 1];
                matrix[j][n - i - 1] = temp;
            }
        }
    }
};

8)零矩阵
编写一种算法,若M × N矩阵中某个元素为0,则将其所在的行与列清零。

    void setZeroes(vector<vector<int>>& matrix) {
        int N = matrix.size();
        int M = matrix[0].size();
        vector <bool> rowzero(N);
        vector <bool> colzero(M);
        // 做两轮循环,第一轮记录 有哪些列存在0;哪些行存在0;
        for (int i = 0; i < N; i++) {
            for (int j = 0; j < M; j++) {
                if (matrix[i][j] == 0) {
                    rowzero[i] = true;
                    colzero[j] = true;
                }
            }
        }
        // 第二轮循环,如果此行有0就全部清零,此行无零,就把存在0的列情理掉。
         for (int i = 0; i < N; i++) {
            for (int j = 0; j < M; j++) {
                if (rowzero[i] == true || colzero[j] == true) {
                    matrix[i][j] = 0;
                }
            }
        }       
    }

9)字符串旋转
字符串轮转。给定两个字符串s1和s2,请编写代码检查s2是否为s1旋转而成(比如,waterbottle是erbottlewat旋转后的字符串)。

// 解法一:遍历位置匹配翻转规律
    bool checkStrSame(string &s1, string &s2,
        int s1_pos, int s2_pos, int len){
        for (int i = 0; i < len; i++) {
            if (s1[s1_pos + i] != s2[s2_pos + i]) {
                return false;
            }
        }
        return true;
    }
    bool isFlipedString(string s1, string s2) {
        int len1 = s1.size();
        int len2 = s2.size();
        if (len1 != len2) {
            return false;
        }
        if (s1 == s2) {
            return true;
        }
        for (int i = 0 ; i < len1; i++) {
            // 找选择中心,从i位置开始转
            // i之后的字符串为新串的head,0-i的字符串为新串尾部
            if (checkStrSame(s1, s2, i + 1, 0, len1 - i - 1) == true &&
                checkStrSame(s1, s2, 0, len1 - i - 1, i + 1) == true) {
                return true;
            }
        }
        return false;
    }
 // 解法二 合并查找
     bool isFlipedString(string s1, string s2) {
        return s1.size()==s2.size()&&(s1+s1).find(s2)!=-1;
    }

2、模拟练习题-leetcode类型高频题目

1)盛最多雨水的容器
给定一个长度为 n 的整数数组 height 。有 n 条垂线,第 i 条线的两个端点是 (i, 0) 和 (i, height[i]) 。找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。返回容器可以储存的最大水量。说明:你不能倾斜容器。

    int maxArea(vector<int>& height) {
        // 双指针。移动策略:优先移动短板指针, 复杂度 时间O(n)/空间O(1)
        // 雨水面积area=min(height[m], heght[n])*(m-n)
        int left = 0;
        int right = height.size() - 1;
        int ret = 0;
        while (left < right) {
            int ans = min(height[right], height[left]) * (right - left);
            ret = max(ret, ans);
            if (height[left] < height[right]) {
                left++;
            } else {
                right--;
            }
        }
        return ret;

本题核心在于为什么双指针做法是对的。先推导基本雨水公式area=min(height[m], heght[n])*(m-n),思考把指针放到两侧,如果要移动一边的柱子大小变化趋势,1)无论移动左边还是右边,宽度m-n一定是变小的;2)高度变化规律 假如移动的是高的一侧,高度由左右最短的决定 而 短的没有动,因此移动面积宽变小高变小只会更小,选择任意一个柱子结果都如此,因此当前短柱子和任意柱子组合排除,想要更大只能尝试移动另一侧。3)如果移动的是短的这侧,则由可能出现更高的。

2)三数之和
给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != j、i != k 且 j != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0 。请你返回所有和为 0 且不重复的三元组。
注意:答案中不可以包含重复的三元组。

class Solution {
public:
    vector<vector<int>> threeSum(vector<int>& nums) {
        // 思路:排序加双指针
        // 三个数加起来为0,特殊情况3个都为0,否则一定有正有负
        // K指向末尾为目标值,i和j求和-num[K];用双指针发简化
        vector<vector<int>> result;
        if (nums.size() < 3) {
            return result;
        }
        
        sort(nums.begin(), nums.end());
        int i = 0, j = 0, k = 0;
        for (k = nums.size() - 1; k >= 2; k--) {
            if (k < nums.size() - 1 && (nums[k] == nums[k + 1])) {
                continue;
            }
            int i= 0, j = k - 1;           
            while (i < j) {
                if (nums[i] + nums[j] < -nums[k]) {
                    i++;
                } else if (nums[i] + nums[j] > -nums[k]) {
                    j--;
                } else {
                    vector<int> tmp = {nums[i], nums[j], nums[k]};
                    result.push_back(tmp);
                    j--;
                    while (j > 0 && nums[j] == nums[j+1]) {
                        j--;
                    }             
                }
            }
        }
        return result;
    }
};

3)除自身以外的数组乘积
给你一个整数数组 nums,返回 数组 answer ,其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积 。
题目数据 保证 数组 nums之中任意元素的全部前缀元素和后缀的乘积都在 32 位 整数范围内。
请 不要使用除法,且在 O(n) 时间复杂度内完成此题。

class Solution {
public:
    vector<int> productExceptSelf(vector<int>& nums) {
        // 思路:单独算前缀 后缀,累加乘法
        // 需要O(n)完成,就不能排序。如果全乘一遍 又需要用到除法
        // 就只能单独记录前缀和后缀,然后用乘法得结果
        // 由于前缀和之间存在迭代关系,可以用一个变量记录。后缀类似
        int premul = 1;
        int aftermul = 1;
        int len = nums.size();
        vector<int> result(len, 1);
        for (int i = 0; i < len; i++) {
            result[i] *= premul;
            premul *= nums[i];
            result[len - 1 - i] *= aftermul;
            aftermul *= nums[len - 1 - i];
        }
        return result;
    }
};

4) 买卖股票的最佳时间
给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。
你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        // 思路:遍历序列,记录之前最低价格,计算当前点卖得到当天最大收益
        int minPrice = prices[0];
        int result = 0;
        for (int i = 0; i < prices.size(); i++) {
            int profit = prices[i] - minPrice;
            if (result < profit) {
                result = profit;
            }
            if (prices[i] < minPrice) {
                minPrice = prices[i];
            }
        }
        return result;
    }
};

5) 数组拆分
给定长度为 2n 的整数数组 nums ,你的任务是将这些数分成 n 对, 例如 (a1, b1), (a2, b2), …, (an, bn) ,使得从 1 到 n 的 min(ai, bi) 总和最大。返回该 最大总和 。

    int arrayPairSum(vector<int>& nums) {
        // 思路:重点在推导,贪心思路
        // 先排序,最小这个值a0和谁比都更小,肯定会放到结果中
        // 那么和他选一起一组的就必须丢弃一个,选最小就是挨着的a1
        // 继续在剩下里面找策略不变,最终a0、a2...an-2被选上
        sort(nums.begin(), nums.end());
        int result = 0;
        for (int i = 0; i < nums.size(); i += 2) {
            result += nums[i];
        }
        return result;
    }

6)寻找两个有序数组的中位数
给定两个大小分别为 m 和 n 的正序(从小到大)数组 nums1 和 nums2。请你找出并返回这两个正序数组的 中位数 。
算法的时间复杂度应该为 O(log (m+n)) 。

    int getKthElement(vector<int>& nums1, vector<int>& nums2, int k) {
        // pos1和pos2为两个数组的index,通过移动pos1和pos2找到第K大的数
        // 随着pos加大 丢掉pos前小于K的数,相应K减少 从新的pos开找K大的
        int len1 = nums1.size();
        int len2 = nums2.size();
        int pos1 = 0;
        int pos2 = 0;
        while (true) {
            // 其中一个数组找到边界
            if (pos1 == len1) {
                return nums2[pos2 + k - 1];
            }
            if (pos2 == len2) {
                return nums1[pos1 + k - 1];
            }
            if (k == 1) {
                return min(nums1[pos1], nums2[pos2]);
            }
            int newpos1 = min(pos1 + k / 2 - 1, len1 - 1);
            int newpos2 = min(pos2 + k / 2 - 1, len2 - 1);
            if (nums1[newpos1] < nums2[newpos2]) {
                k -= newpos1 - pos1 + 1;
                pos1 = newpos1 + 1;
            } else {
                k -= newpos2 - pos2 + 1;
                pos2 = newpos2 + 1;
            }
        }
    }
    double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
        // 思路:二分查找逻辑 来实现log()时间效率
        // 如果总数是奇数为中间数,总数为偶数为中间两个数的均值
        // 定义getKthElement为两个数组一起第K大的数
        int len = nums1.size() + nums2.size();
        if (len % 2 == 1) {
            return getKthElement(nums1, nums2, (len + 1) / 2);
        } else {
            return (getKthElement(nums1, nums2, len / 2) + getKthElement(nums1, nums2, len / 2 + 1)) / 2.0;
        }   
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值