每每刷完一道题后,其思想和精妙之处没有地方记录,本篇博客用以记录刷题过程中的遇到的算法和技巧
001)两数之和
给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的两个整数。
你可以假设每种输入只会对应一个答案。但是,你不能重复利用这个数组中同样的元素。
给定 nums = [2, 7, 11, 15], target = 9
因为 nums[0] + nums[1] = 2 + 7 = 9
所以返回 [0, 1]
typedef struct {
int value;
int pos;
}num_data_t;
//use qsort to sort this nums array
int struct_cmp(const void *arr1, const void *arr2)
{
num_data_t *arr1_tmp = (num_data_t *)arr1;
num_data_t *arr2_tmp = (num_data_t *)arr2;
return (arr1_tmp->value - arr2_tmp->value);
}
int* twoSum(int* nums, int numsSize, int target, int* returnSize){
int *result = NULL;
num_data_t *num_data = NULL;
int left = 0;
int right = 0;
num_data = (num_data_t *)malloc(numsSize * sizeof(num_data_t));
if(num_data == NULL) {
return result;
}
for (int i = 0; i < numsSize; i++) {
num_data[i].value = nums[i];
num_data[i].pos = i;
}
//0. due to need to return position in nums, so arrange a structure
//1. sort this nums array
qsort(num_data, numsSize, sizeof(num_data_t), struct_cmp);
left = 0;
right = numsSize - 1;
//2. use two pointer, and move the left pointer if sums less than targets
while (left < right) {
if (num_data[left].value + num_data[right].value > target) {
right--;
}
else if(num_data[left].value + num_data[right].value < target) {
left++;
}
else if (num_data[left].value + num_data[right].value == target) {
result = (int *)malloc(2 * sizeof(int));
result[0] = num_data[left].pos;
result[1] = num_data[right].pos;
free(num_data);
*returnSize = 2;
return result;
}
}
return result;
}
此题利用了qsort先排好顺序后,使用移动左右两个指针来进行查找,当前仅是找到一对满足条件的数组而已。算法思想在于,当和小于target时,移动left指针,当和大于target时,移动right指针,while循环条件是left < right(对撞指针)。
000)统计一个数中二进制表示0或者1出现的个数方法:
075)颜色分类法:
给定一个包含红色、白色和蓝色,一共 n 个元素的数组,原地对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。
此题中,我们使用整数 0、 1 和 2 分别表示红色、白色和蓝色
本题由于是特殊 0 1 2 处理即可,可以不使用qsort来进行排序。具体做法是先使用桶排序对 0 1 2 的数进行计算,然后在nums上直接进行填数,时间复杂度是:O(n),需要遍历一次数组,空间复杂度为 O(K)级别,K为3(即需要一个桶来对整数0-2进行排序,本题使用count[2]这样一个数组)
void sortColors(int *nums, int numsSize){
int count[3] = {0};
int i = 0;
int j = 0;
// 借助桶排序,计算有多少个0/1/2
for (i = 0; i < numsSize; i++) {
if (nums[i] > 2) {
printf("inputs error");
}
count[nums[i]]++;
}
//将计算好后的0-1-2的个数,回填至nums数组
for(i = 0; i < count[0]; i++) {
nums[j] = 0;
j++;
}
for(i = 0; i < count[1]; i++) {
nums[j] = 1;
j++;
}
for(i = 0; i < count[2]; i++) {
nums[j] = 2;
j++;
}
}
283)移动零
给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。
输入: [0,1,0,3,12]
输出: [1,3,12,0,0]
//法一:将数组非零值拷贝至一个新的数组,时间复杂度:O(n),空间复杂度:O(n)
//法二:[0.......k)为存放非零的位置, [k,numsSize]为存放0
void moveZeroes(int *nums, int numsSize) {
int k = 0; //[0.......k)为存放非零的位置, [k,numsSize]为存放0
int i = 0;
//遍历一次数组,将非零值都移动到[0,k)里
for (i = 0; i < numsSize; i++) {
if (nums[i] != 0) {
nums[k] = nums[i];
k++;
}
}
//将超过k值的,赋值为0,[k, numsSize] 为0
for (i = k; i < numsSize; i++) {
nums[i] = 0;
}
return ;
}
125)验证回文串
给定一个字符串,验证它是否是回文串,只考虑字母和数字字符,可以忽略字母的大小写。
说明:本题中,我们将空字符串定义为有效的回文串。
示例 1:
输入: “A man, a plan, a canal: Panama”
输出: true
示例 2:
输入: “race a car”
输出: false
解题思路,利用对撞指针,不过由于需要处理字符,可以先处理好大写字母转换为小写后和空格后,再利用对撞指针处理即可。
580)连续子区间和
题目描述
给一串含有c个正整数的数组, 求出有多少个下标的连续区间, 它们的和大于等于x。
解答要求
时间限制:1000ms, 内存限制:64MB
输入
第一行两个整数c x(0 < c <= 1000000, 0 <= x <= 100000000)
第二行有c个正整数(每个正整数小于等于100)。
输出
输出一个整数,表示所求的个数。
样例
输入样例 1 复制
3 6
2 4 7
输出样例 1
4
本题是利用双指针来实现,思路清晰, 但是 index1 和 index2 都只需要遍历一次即可,不需要回退,只要涵盖了回退操作,都不是最优解。另外,注意如果是 输出数字,有可能需要输出%ld的情况。
printf("%ld", result);
unsigned long calcSum1(unsigned int *num, unsigned int n, unsigned int m)
{
unsigned int index1 = 0;
unsigned int index2 = 0;
unsigned long sum = num[0];
unsigned long count = 0;
unsigned long result = 0;
// 最直接能想到的方法:遍历两次数组 O(n^2),但是其中有很多可以优化的地方
for (index1 = 0; index1 <= n; index1++) {
count = 0;
count = num[index1];
for (index2 = index1 + 1; index2 <= n; index2++) {
if (count >= m) {
result++;
}
count = count + num[index2];
}
}
return result;
}
unsigned long calcSum2(unsigned int *num, unsigned int n, unsigned int m)
{
unsigned int index1 = 0;
unsigned int index2 = 0;
unsigned long sum = num[0];
unsigned long count = 0;
unsigned long result = 0;
for (index1 = 0; index1 <= n; index1++) {
count = 0;
count = num[index1];
for (index2 = index1 + 1; index2 <= n; index2++) {
if (count >= m) {
// 优化1:如果找到了超过m的index2 由于数组都是正数,后续数可以不用再遍历,直接退出即可
result = result + n - index2 + 1;
break;
}
count = count + num[index2];
}
}
return result;
}
unsigned long calcSum3(unsigned int *num, unsigned long n, unsigned long m)
{
unsigned long index1 = 0;
unsigned long index2 = 1;
unsigned long sum = num[0];
unsigned long count = 0;
unsigned long result = 0;
count = num[index1];
for (index1 = 0; index1 <= n; index1++) {
for (index2; index2 <= n; index2++) { // 优化2:index2 在找到 index1 超过m时不用回到 index1+1 的位置,继续往下走即可
if (count >= m) {
// 优化1:如果找到了超过m的index2 由于数组都是正数,后续数可以不用再遍历,直接退出即可
result = result + n - index2 + 1;
count = count - num[index1];
break;
}
count = count + num[index2];
}
}
return result;
}
本题是利用双指针来实现,思路清晰, 但是 index1 和 index2 都只需要遍历一次即可,不需要回退,只要涵盖了回退操作,都不是最优解。下面再给出另外一种思路,index1 和 index2 都只遍历了一次,复杂度为O(n)。
unsigned long calcSum4(unsigned int *num, unsigned long n, unsigned long m)
{
unsigned long index1 = 0;
unsigned long index2 = 0;
unsigned long count = 0;
unsigned long result = 0;
count = num[index1];
while(index2 < n && index1 < n) {
//count < m 时,那就加 num[index2]
if (count < m || index1 > index2) {
index2++;
count = count + num[index2];
} else { //count >= n 时,那就减 num[index1]
result = result + n - index2;
count = count - num[index1];
index1++;
}
}
return result;
}
512. Word Maze
题目描述
Word Maze 是一个网络小游戏,你需要找到以字母标注的食物,但要求以给定单词字母的顺序吃掉。假设给定单词if,你必须先吃掉i然后才能吃掉f。
但现在你的任务可没有这么简单,你现在处于一个迷宫Maze(n×m的矩阵)当中,里面到处都是以字母标注的食物,但你只能吃掉能连成给定单词W的食物。
注意区分英文字母大小写,并且你只能上下左右行走。
5 5
SOLO
CPUCY
EKLQH
CRSOL
EKLQO
PGRBC
输出样例 1
YES
此题解题思路是利用DFS
1)先右后下,遍历整个输入矩阵,若找到满足条件的字符,则标记该点走过
2)接着依次遍历该点的上下左右四个点,函数入口处,判断当前已遍历点数和 target相等,认为已遍历完成,可以走通,输出Yes
3)如果该点在遍历上下左右四个点,均未走通
4)基于该位置点,将标志清空,查找下一个点
难点是出现下面这种情况的处理:
3 3
SOL //这一行是 target ,需要找到的目标串
SOB //maze的第一行
ABL //maze的第二行
CSO //maze的第三行
1)第一个’S’,字符就已经匹配上了,将‘S’标记为已访问,进入以下上右左的方式查找’O’
2)在向右的遍历中发现了’O’,匹配上了,将‘O’标记为已访问,接着再以下上左右的方式去查找‘L’,发现没有找到
3)这时将’O’,访问标记删除掉,回到‘S’的递归层次来,刚才查找‘O’是通过右查找到的,接着进入左方式查找’O’,无法找到,至此,下上右左的查找都已完成,都没有找到,此时将‘S’访问标记删除,当前递归循环完成,返回结论是以当前’S’点无法找到子串,所以退出,下面开始从’O’,查找target字符串 ‘SOLO’。
350. 两个数组的交集 II
给定两个数组,编写一个函数来计算它们的交集。
示例 1:
输入: nums1 = [1,2,2,1], nums2 = [2,2]
输出: [2,2]
示例 2:
输入: nums1 = [4,9,5], nums2 = [9,4,9,8,4]
输出: [4,9]
法一:
当输入数据是有序的,推荐使用此方法。在这里,我们对两个数组进行排序,并且使用两个指针在一次扫面找出公共的数字。
int cmp(const void *a,const void *b){//编写比较函数, a > b 返回 1 ;是默认的从小到大的排序方法
if( *(int*)a > *(int*)b) return 1;
else if(*(int*)a < *(int*)b) return -1;
else if(*(int*)a == *(int*)b) return 0;
return 0;
}
int* intersect(int *nums1, int nums1Size, int *nums2, int nums2Size, int* returnSize){
qsort(nums1,nums1Size,sizeof(nums1[0]),cmp);
qsort(nums2,nums2Size,sizeof(nums2[0]),cmp);
int t = 0;
for (int p = 0,q = 0; ((p < nums1Size) && (q < nums2Size)) ;){
if (nums1[p] == nums2[q]) {
nums1[t] = nums1[p];
t++;
p++;
q++;
}
else if (nums1[p]>nums2[q]){
q++;
}
else {
p++;
}
}
*returnSize = t;
int *temp = (int *)malloc(t*sizeof(int));
if (!temp) {
printf("fail to malloc !");
return NULL;
}
for (int j = 0; j<t; j++){
temp[j] = nums1[j];
}
return temp;
}
法二:
思路:此题可以看成是一道传统的映射题(map映射),为什么可以这样看呢,因为我们需找出两个数组的交集元素,同时应与两个数组中出现的次数一致。这样就导致了我们需要知道每个值出现的次数,所以映射关系就成了<元素,出现次数>,所以我们可以首先统计数组1中所有元素的出现次数。然后再遍历数组2,如果数组2中的元素在map中存在(出现次数大于0),该元素就是一个交集元素,我们就将其存入返回数组中并且将map中该元素的出现次数减一即可.
class Solution {
public:
vector<int> intersect(vector<int>& nums1, vector<int>& nums2) {
vector<int>rec;
unordered_map<int,int>map;
for(int i =0;i<nums1.size();i++) //首先统计数组1中所有元素的出现次数
map[nums1[i]]+=1;
for(int i =0;i<nums2.size();i++) //遍历数组2
if(map[nums2[i]]>0)
{
rec.push_back(nums2[i]);
map[nums2[i]]-=1; //将map中该元素的出现次数减一即可
}
return rec;
}
};
454. 四数相加 II
给定四个包含整数的数组列表 A , B , C , D ,计算有多少个元组 (i, j, k, l) ,使得 A[i] + B[j] + C[k] + D[l] = 0。
为了使问题简单化,所有的 A, B, C, D 具有相同的长度 N,且 0 ≤ N ≤ 500 。所有整数的范围在 -228 到 228 - 1 之间,最终结果不会超过 231 - 1 。
输入:
A = [ 1, 2]
B = [-2,-1]
C = [-1, 2]
D = [ 0, 2]
输出:
2
解释:
两个元组如下:
#include
/*
* 使用hash算法,O(n^2)
*/
typedef struct hlist {
int nkey, pkey;
int nNum, pNum;
struct hlist *next;
}*hnode;
hnode newHashNode(int key)
{
hnode n = (hnode)calloc(1, sizeof(struct hlist));
if (key < 0){
n->nkey = key;
n->pkey = 0 - key;
n->nNum = 1;
}else{
n->pkey = key;
n->nkey = 0 - key;
n->pNum = 1;
if (key == 0)
n->nNum = 1;
}
return n;
}
//键值即取余
int hashKey(int s)
{
if (s < 0)
return (0 - s) % 10000;
else
return s % 10000;
}
int fourSumCount(int* A, int ASize, int* B, int BSize, int* C, int CSize, int* D, int DSize){
int i,j,n = 0,s, key;
hnode hash[10000] = {0}, prev, curr, new;
//特殊情况处理
if (ASize == 0 || BSize == 0 ||CSize == 0 || DSize == 0)
return 0;
//遍历AB和,将其加入到hash桶中,和为key,值为次数
for (i = 0; i < ASize; i++)
{
for (j = 0; j < BSize; j++)
{
s = A[i] + B[j];
key = hashKey(s);
prev = NULL;
curr = hash[key];
while (curr)
{
if (curr->nkey == s) {
curr->nNum++;
break;
}
else if (curr->pkey == s) {
curr->pNum++;
break;
}
else {
prev = curr;
curr = curr->next;
}
}
if (!curr) {
new = newHashNode(s);
if (prev)
prev->next = new;
else
hash[key] = new;
}
}
}
//遍历CD和s,在hash表中查找-s,统计次数。
for (i = 0; i < CSize; i++)
{
for (j = 0; j < DSize; j++)
{
s = C[i] + D[j];
s = 0 - s;
key = hashKey(s);
curr = hash[key];
while (curr)
{
if (curr->nkey == s){
n += curr->nNum;
break;
} else if (curr->pkey == s){
n += curr->pNum;
break;
} else
curr = curr->next;
}
}
}
return n;
}
447. 回旋镖的数量
给定平面上 n 对不同的点,寻找有多少个这些点构成的三元组,表示的元组 (i, j, k) ,其中 i 和 j 之间的距离和 i 和 k 之间的距离相等(需要考虑元组的顺序)。
找到所有回旋镖的数量。你可以假设 n 最大为 500,所有点的坐标在闭区间 [-10000, 10000] 中。
示例:
输入:
[[0,0],[1,0],[2,0]]
输出:
2
解释:
两个回旋镖为 [[1,0],[0,0],[2,0]] 和 [[1,0],[2,0],[0,0]]
int numberOfBoomerangs(vector<pair<int, int>>& points) {
int res = 0;
//遍历所有的点,建立以该i点为枢纽点,其他点到该点的距离为hash值
for (int i = 0; i != points.size(); i++){
unordered_map<int, int> hashmap;
for (int j = 0; j != points.size(); j++){
if (i != j)
hashmap[dis(points[i], points[j])]++;
}
//遍历该i点,如果相同距离的点超过了两个,则符合条件,记录到res里
for (auto it = hashmap.begin(); it != hashmap.end(); it++){
if (it -> second >= 2)
res += (it -> second) * (it -> second - 1); //Anm 排列组合的选择
}
}
return res;
}
//由于距离需要开平方,这里不做该处理,直接用也可以反映真实距离的大小,不存在浮点误差的问题
int dis(const pair<int, int> &p1, const pair<int, int> &p2){
return (p1.first -p2.first) * (p1.first -p2.first) +
(p1.second - p2.second) * (p1.second - p2.second);
}
动态规划 – 自下而上的解决问题;记忆化搜索,自顶向下的解决问题。
– 动态规划–将原问题拆解成若干子问题,同时保存子问题的答案,使每个子问题只求解一次,最终获得原问题的答案。
70. 爬楼梯
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
注意:给定 n 是一个正整数。
示例 1:
输入: 2
输出: 2
解释: 有两种方法可以爬到楼顶。
- 1 阶 + 1 阶
- 2 阶
示例 2:
输入: 3
输出: 3
解释: 有三种方法可以爬到楼顶。- 1 阶 + 1 阶 + 1 阶
- 1 阶 + 2 阶
- 2 阶 + 1 阶
// 递归
int climbStairs(int n){
if (n <= 2) {
return n;
}
return climbStairs(n - 1) + climbStairs(n - 2);
}
// 记忆化搜索 -- 自顶往下
int memo[64] = {0};
int climbStairs(int n) {
if (n <= 2) {
return n;
}
// 如果满足条件则直接返回记忆数组里的值,减少递归次数
if (n < 64 && memo[n] != 0) {
return memo[n];
}
// 不满足条件才进行递归 O(n)
memo[n] = climbStairs(n-1) + climbStairs(n-2);
return memo[n];
}
// 动态规划 -- 自下往上
int climbStairs(int n){
int i, a1, a2, res;
if (n <= 2) {
return n;
}
a1 = 1;
a2 = 2;
res = 0;
// 自下而上的进行计算,动态规划
for (i = 3; i <= n; i++) {
res = a1 + a2;
a1 = a2;
a2 = res;
}
return res;
}
343. 整数拆分
给定一个正整数 n,将其拆分为至少两个正整数的和,并使这些整数的乘积最大化。 返回你可以获得的最大乘积。
输入: 10
输出: 36
解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36。
思路是:1)先思考自顶往下的记忆化搜索的方式借助常规的递归的思路,但是依靠记忆化数组,减少递归次数,就有了记忆化搜索的方法。
2)再思考动态规划的方式,自底向上的方式,由小至多累加上去,直接想动态递归的思路较难。
// 记忆化搜索 -- 自顶往下
int memo[64] = {0}; // memo[i]表示将i分割后可以获得的最大乘积
int integerBreak(int n) {
int res = -1;
if (n == 1 || n == 2) { //memo[1] || memo[2] == 1
return 1;
}
if (memo[n] != 0) {
return memo[n];
}
// res i*(n-i) i*integerBreak(n-i)
// 1 1*2 1*1
// 2 2*1 2*1
for (int i = 1; i <= n-1; i++) { // memo[3] == 2
res = max3(res, i*(n-i), i*integerBreak(n-i));
}
memo[n] = res;
return res;
}
326. 3的幂
bool isPowerOfThree(int n) {
if (n <= 0) return false;
if (n == 1) return true;
while(1) {
if(n == 1)
return true;
else if (n % 3 > 0)
return false;
else{
n = n / 3;
}
}
}