剑指offer数据结构与算法前三章(001-020)

本文深入探讨了各种算法问题的解决方案,包括使用位操作进行除法优化、字符二进制加法、计算二进制1的个数、查找只出现一次的数字、无重复字符字符串长度的最大乘积、排序数组中两数之和、数组中和为0的三个数等。通过位运算和哈希表等方法,提高了算法效率和准确性。
摘要由CSDN通过智能技术生成

001

  • 这题不能用乘除,我们用加减,每次将除数翻倍,与被除数比较大小,若被除数小于此时的翻倍结果,则让被除数减去这个未翻倍前的除数,假设是k*除数,记录此时的商加等于k;
  • 这种算法首先要保证被除数和除数符号相同,我们先把它们都转换成负数(负数转正数有可能溢出)。
  • int型的范围是-2 31~-2 31-1,当被除数是-2 31,除数是-1时,得到的结果超出范围,要返回最大的整数值 Integer.VALUE。
  • 十六进制中负数的二进制原码的最高位是符号位,后面的31位为序号位,不是值位。1后面的000 0000 0000 0000 0000 0000 0000 0000,表示序号1,表示负数中,从小到大的第一位
    • 由于int的最小值为-231,排在负数从小到大的序号1,所以int i = 0x80000000 输出为 - 231
      比如:我们来看看0xFFFFFFFF,原码 1111 1111 1111 1111 1111 1111 1111 1111,最高位为1 ,为负数,序号位为第231-1位 (111 1111 1111 1111 1111 1111 1111 1111= 231-1, 所以0xFFFFFFFF为负数从小到大 第2 31-1位 ,即 -2 31+2 31-1= -1 ;
      再比如求-2 31的一半,它也就是负数中从小到大第2 30个,原码为 -231+230 = -230,为0xc0000000。
int divide(int dividend, int divisor) { //被除数,除数
	if (dividend == 0x80000000 && divisor == -1) {
		return INT_MAX;
	}
	int flag = 2; //标记最终结果的正负,等于1为负
	if (dividend > 0) {
		flag--;
		dividend = -dividend;
	}
	if (divisor > 0) {
		flag--;
		divisor = -divisor;
	}
	int result = 0;  //最终结果;
	int quotient;  //暂时记录的倍数
	int value;  //记录翻倍的除数
	while (dividend <= divisor) {
		value = divisor;
		quotient = -1;//考虑到结果可能为-2的31次方,所以用负数存
		while (value >= 0xc0000000 && dividend <= value + value) {
			value += value;
			quotient += quotient;
		}
		result += quotient;
		dividend -= value;
	}
	if (flag != 1)
		result = -result;
	return result;
}

002

字符二进制加法,可以参考10进制加法直接计算,从低位开始取出字符相加,记录进位。
每次将计算结果添加到字符中。
c++提供字符操作函数,字符串后面可以直接用+号连接其他字符或串, (串).size()可以获得串长,这个长度不包括末尾表示符。以及reverse(串)函数反转字符串,cout<<(串)可以直接打印串。

string addBinary(string a, string b){
	int Ai, Bj;
	int i = a.size()-1, j = b.size()-1;
	int carry = 0;
	int sum;
	string result="";
	while (i >= 0 || j >= 0){
		if (i >= 0)
			Ai = (int)a[i--] - '0';
		else
			Ai = 0;
		if (j >= 0)
			Bj = (int)b[j--] - '0';
		else
			Bj = 0;

		sum = Ai + Bj + carry;
		if (sum >= 2)
		{
			carry = 1;
			sum -= 2;
		}
		else
			carry = 0;
		result = result + (char)('0'+sum);
	}
	if (carry == 1)
		result =  result+'1';
	reverse(result.begin(),result.end());
	return result;
}

003二进制1的个数

vector <int>countBits(int n) {
	vector <int> bit(n+1);
	bit[0] = 0;
	for (int i = 0; i <= n; i++) 
		bit[i] = bit[i / 2] + i % 2;
	//bit[i] = bit[i >> 1] + (i &1) ;位运算效率更高
	return bit;
}

004只出现一次的数字

方法1哈希

int singleNumber(vector<int>& nums) {
	unordered_map<int,int> sum;
	int i;
	for (i = 0; i < nums.size(); i++) {
		++sum[nums[i]];
	}
	for (i = 0; i < nums.size(); i++)
		if (sum[nums[i]] == 1)
			return nums[i];
}

004 方法二 除余

将所有数据的相同二进制位相加,只出现一次的数据的i位上1or0和总和%3相反

int singleNumber2(vector<int>& nums) {
	int bit[32] = { 0 };
	for (int num = 0; num < nums.size(); num++) {
		for (int i = 0; i < 32; i++) {
			bit[i] += (nums[num] >> (31 - i)) & 1;//判断该数字的倒数i位为0还是1
		}
	}
	int result=0;
	for (int i = 0; i < 32; i++)
		result = (result << 1) + bit[i] % 3;
	return result;
}

005 无重复字符的字符串长度最大乘积

int maxProduct(vector<string>& words) {
	int i,j;
	vector<int> flag(words.size(),0);
	int result = 0;
	for (i = 0; i < words.size(); i++) {
		for (j =0; j < words[i].size(); j++) {
			flag[i] |= 1 << (words[i][j] - 'a'); 
			//注意是将1向左移动到位置上,再或运算
			//保证与拿来的flag不会变,而且修改对应位置为1
		}
	}
	for (i = 0; i < words.size(); i++) {
		for (j = i + 1; j < words.size(); j++) {
			if ((flag[i] & flag[j]) == 0)
				result = max(result, (int)(words[i].size() * words[j].size()));
		}
	}
	return (result > 0 ? result : NULL);
}

006 排序数组中两个数字之和

vector<int> twoSum(vector<int>& numbers, int target) {
	vector<int> result;
	int i, j;
	i = 0, j = numbers.size()-1;
	while (numbers[i] + numbers[j] != target && i < j) {
		if (numbers[i] + numbers[j] > target)
			--j;
		else
			++i;
	}
	result.push_back(i);
	result.push_back(j);
	return result;
}

007 数组中和为0的三个数

类似于006,先排序,然后选定一个数字target,在序列中找相加等于-target的两个数

vector<vector<int>> threeSum(vector<int>& nums) {
	int i, j, k;
	vector<vector<int>> result;
	sort(nums.begin(), nums.end()); //从小到大排序
	//nums[k]为选定的值
	if (nums.size() >= 3) {
		for (k = 0; k < nums.size(); k++) {
			//nums[k]也要去重
			while (k > 0 && k < nums.size() - 1 && nums[k] == nums[k - 1])
				++k;
			i = k + 1;
			j = nums.size() - 1;
			while (i < j) {
				if (nums[i] + nums[j] == -nums[k]) {
					result.push_back({ nums[k],nums[i],nums[j] });
					//寻找时去重
					while (i < j - 1 && nums[i] == nums[i + 1])
						++i;
					while (i < j - 1 && nums[j] == nums[j - 1])
						--j;
					++i;
					--j;
				}
				else if (nums[i] + nums[j] > -nums[k])
					--j;
				else
					++i;
			}
		}
	}
	return result;
}

008

思路,从第一个数据开始,i存头,j存尾,
如果不满足条件,就把j往后搜
若满足条件,把i+1
要注意题目是 连续的数组,

int minSubArrayLen(int target, vector<int>& nums) {
	int i, j;  //i存高位,j低位
	int result = 66536; //结果
	int sum;  //当前数据和
	if (nums.size() != 0) {
		i = 0;
		j = i;
		sum = nums[i];
		while (i<nums.size()&&j < nums.size()) {
			if (sum < target){
				++j;
				if (j >= nums.size()) {
					if (i == 0)
						result = 0;
					break;
				}
				sum += nums[j];
			}
			else {
				result = min(result,j - i + 1);
				sum -= nums[i];
				++i;
			}
		}
	}
	return result;
}

009

思路类似于008
从头开始扫描,若ij<target,result++ ,需要注意的是,i=j时,用其中一个乘以1,不要重复乘
每次满足条件,j往后移动一个,sum=sum
nums[j]
不满足条件,i移动, sum=sum/nums’[i]

int numSubarrayProductLessThanK1(vector<int>& nums, int k) {
	int i, j;
	int sum;
	int result = 0;
	i = 0;
	j = i;
	sum = nums[i];
	while (i < nums.size()) {
		if (sum < k) {  //满足条件
			if (j + 1 < nums.size()) {
				++result;
				++j;
				sum = sum * nums[j];
			}
			else {
				result += (j - i) * (j - i + 1) / 2 + 1;
				break;
			}
		}
		else {
			++i;
			sum = nums[i];
			j = i;
		}
	}
	return result;
}

009 优化

对现有的乘积小于k的序列ABCD,我们可以统计已D为 末尾的个数,这样我们只需要把j从头到尾走一遍,不需要回溯,观察易得以nums[j]为结尾的满足条件序列为j-i+1

int numSubarrayProductLessThanK(vector<int>& nums, int k) {
	int i, j, sum;
	int result = 0;
	i = 0,j = i,sum = 1;
	for (j = 0; j < nums.size(); j++) {
		sum *= nums[j];
		while (i <= j && sum >= k) {
			sum /= nums[i++];
		}
		if (i <= j)
			result += j - i + 1;
	}
	return result;
}

010 和为k的子数组

只适用于正数

同样是双指针,用i,j指向头和尾,当sum>k时,i向后移,缩小移动窗口,直到不满足移动条件,
当sum<k时,j向后移动,增大移动窗口,直到不满足移动条件
当sum=k时,i和j都向后移动一位

int subarraySum1(vector<int>& nums, int k) {
	int i, j, sum, result;
	i = 0;
	j = 0;
	sum = nums[i];
	result = 0;
	while (i < nums.size() && j < nums.size()) {
		if (sum < k) {
			if(nums[j]>=0)
				++j;
			else 

			if (j >= nums.size())
				break;
			else
				sum += nums[j];
		}
		else if (sum > k) {
			sum -= nums[i];
				++i;
				if (i >= nums.size())
					break;
		}
		else if (sum == k) {
			result++;
			sum -= nums[i];
			++i;
			++j;
			if (i >= nums.size()||j>=nums.size())
				break;
			sum += nums[j];
		}
	}
	return result;
}

正负数都有的正确题解

  • 用sum表示包括当前数字在内的前面所有数字之和,连续的数组相加为k,说明sum[j]-sum[i]=k;

  • 那我们对每个数字求前缀之和,然后找到前面已经得到的数据前缀之和中有多少个与之相差k的数字(位置不同), n表示前面有n个这样的nums[i],

  • 注意我们要将hash(0)初始化为1,因为对第一个nums[j]-k=0的数据来说,它到数组起点的是满足条件的。

  • 那么以num[j]结尾的满足条件的子数组有hash(sum[j]-k) = n 个,就可以得到以当前数字结尾的满足k的连续子数组有多少个

  • 所以我们需要一个哈希数组,键为前缀和,值为出现了几次,每次拿到新的数据时,它前面的数据都是已知的

  • c++基础知识 map容器
    相当于python的字典,有键和值
    创建:map<int,int> newmap [= {{1:2} {2:3}};
    插入:newmap[K]= n, 创建了键为k的map对,值为n,默认为0;最好是用insert
    修改:newmap[k]= n,
    查询:n= newmap.count(k),返回k出现的次数

int subarraySum(vector<int>& nums, int k) {
	int sum, result;
	map<int, int> preHash = { {0,1} };
	result = 0, sum = 0;
	for (int num : nums) {
		sum += num;
		//先看hash(sum-k)是否存在,再修改result
		result += preHash.count(sum - k) ? preHash[sum - k] : 0;
		preHash.count(sum) ? ++preHash[sum] : preHash[sum] = 1;
	}
	return result;
}

011 0和1个数相同的最长子数组

  • 数组中只有0和1,我们把所有的0改为-1,求0和1相等的子数组就转化为求和为0的子数组
  • 这和010题有些类似,不过010求的是这样的子数组个数,而011求最长子数组的长度,那我们稍微改变一下,hash表不存出现次数,而是存第一次出现的位置,这样,当再次求得sum-k时,都拿来和第一次出现的位置计算出长度,与当前已有的maxLen, 比较取最大,直到数组遍历结束,
  • 那么我们分析开始遍历前应该初始化hash(0)=-1
int findMaxLength(vector<int>& nums) {
	int i=0, sum=0, maxLen=0;
	map<int, int> indexHash{ {0,-1} };
	for (i = 0; i < nums.size(); i++) {
		sum += (nums[i] == 0) ? -1 : 1;
		if (indexHash.count(sum))
			maxLen = max(maxLen, i - indexHash[sum]);
		else
			indexHash[sum]= i;
	}
	return maxLen;
}

012 左右两边子数组的和相等

int pivotIndex(vector<int>& nums) {
	int i = 0, sum = 0,count = 0;
	for (i = 0; i < nums.size(); i++)
		sum += nums[i];
	for (i = 0; i < nums.size(); i++) {
		if (count == (sum - count - nums[i]))
			return i;
		count += nums[i];
	}
	return -1;
}

013 二维子矩阵的和

  • 观察可得二维子矩阵[r1,c1,r2,c2]的和可由r2c2的二维前缀和减去r2c1,r1c2的前缀和,再加上重复减掉的r1c1前缀和,我们在初始化数组时就将每个位置的前缀和求出来,那么调用二维子矩阵函数的时间复杂度就降到了1
  • 求前缀和,我们可以拿当前位置的同列不同行的presum+当前这一行到这里的和sum
int preSum[200][200];
void NumMatrix(vector<vector<int>>& matrix) {
	if (matrix.size() > 0) {
		preSum[0][0] = matrix[0][0];
		for (int j = 1; j < matrix[0].size(); j++) //第一行
			preSum[0][j] = preSum[0][j - 1] + matrix[0][j];
		for (int i = 1; i < matrix.size(); i++) //第一列
			preSum[i][0] = preSum[i - 1][0] + matrix[i][0];
		//行
		for (int i = 1; i < matrix.size(); i++)  
		{	//列
			for (int j = 1; j < matrix[0].size(); j++) {
				preSum[i][j] = matrix[i][j] + preSum[i-1][j] 
					+ preSum[i][j-1] - preSum[i-1][j-1];
			}
		}
	}
}

int sumRegion(int row1, int col1, int row2, int col2) {
	int result = preSum[row2][col2];
	if (row1 > 0)
		result -= preSum[row1 - 1][col2];
	if(col1>0)
		result -= preSum[row2][col1-1];
	if (row1 > 0 && col1 > 0)
		result += preSum[row1 - 1][col1 - 1];
	return result;
}

014 字符串中的变位词

这里涉及到C++两个容器可以直接比较是否相等

  • 把s1中字符出现的次数用哈希表存起来,到s2中找到一个这样的子串,它的字符哈希表和s1一样
  • 对s2的子串,将它出现的字符在s1中-1,处理完后若s1的哈希表全部为0,说明匹配成功
  • 我们每次都是处理s2中等长的子串,可以用一个固定长度窗口,每次匹配不成功时窗口向后移动一位,对移出窗口的字符+1,进入窗口的字符-1

bool checkInclusion(string s1, string s2) {
vector count(26, 0), zero(26.0);
int i;

	if (s1.length() > s2.length())
		return false;
	for (i = 0; i < s1.length(); i++) {  //初始化哈希表
		++count[s1[i] - 'a'];
		--count[s2[i] - 'a'];
	}
	if (count==zero)  //匹配成功
		return true;
	//从s2中s1长度处开始搜索,这里i每次都是指向滑动窗口的下一位
	for (i = s1.length(); i < s2.length(); i++) {
		//失败,移动窗口向后走
		--count[s2[i] - 'a']; //新加入的s2[i+1]
		++count[s2[i - s1.length()] - 'a'];  //被删掉的s2[i - s1.size()+1]
		if (count == zero)  //匹配成功
			return true;
	}
	return false;
}

015 字符串中的所有变位词

014稍微修改下就可以

vector<int> findAnagrams(string s, string p) {
	vector<int> count(26, 0), zero(26.0), result;
	int i;
	if (s.length() > p.length())
		return result;
	for (i = 0; i < s.length(); i++) {  //初始化哈希表
		++count[s[i] - 'a'];
		--count[p[i] - 'a'];
	}
	if (count == zero)  //匹配成功
		result.emplace_back(0);
	//从s2中s1长度处开始搜索,这里i每次都是指向滑动窗口的下一位
	for (i = s.length(); i < p.length(); i++) {
		//失败,移动窗口向后走
		--count[p[i] - 'a']; //新加入的s2[i]
		++count[p[i - s.length()] - 'a'];  //被删掉的s2[i - s1.size()]
		if (count == zero)  //匹配成功
			result.emplace_back(i - s.length()+1);
	}
	return result;
}

016 不含重复字符的最长字符串

同样用哈希表存子串中出现的字符,新的字符在哈希表中不存在,从第一个字符开始,如果没有重复,移动右指针,如果有重复,移动左指针

int lengthOfLongestSubstring(string s) {
	unordered_map<char, int> count;
	int result = 0;
	int i = 0, j = 0;
	if (s.length() <= 0)
		return 0;
	for (i; i < s.length(); i++) {
		while (j < s.length() && !count.count(s[j]))
			count[s[j++]] = 1;
		result = max(result, j - i);
		count.erase(s[i]);  //注意这里要删除,因为我们判断有无重复的条件是这个字符在哈希表中是否存在
	}
	return result;
}

017 含有所有字符的最短子字符串(s,t)

用哈希表存t中的字符,并记录好出现次数
在s中遍历,
若当前字符在哈希表中,则计数-1,不在哈希比中的字符不处理
若哈希表所有值小于等于0,找到一个满足条件的子串
左指针向后移动一位,对滑出窗口的字符,若存在哈希表中,则计数+1
若处理后不满足条件,右指针向后移动一位

// 哈希表判断函数
bool lessORequalZero(map<char, int> count) {
	for (int i = 0; i < count.size(); i++) {
		if (count[i] > 0)
			return false;
	}
	return true;
}
string minWindow(string s, string t) {
	map<char, int> count;
	string result;
	int min = 65536;
	int i = 0,start=0;
	if (s.length() < t.length())
		return result;
	for (int i = 0; i < t.length(); i++) {
		count[t[i]]++;
		count[s[i]]--;
	}
	if (lessORequalZero(count)) return s.substr(0, t.length());
	for (int i = t.length(); i < s.length(); i++) {
		if (count.count(s[i]))
			count[s[i]]--;
		while (lessORequalZero(count)) {
			if (min > i - start + 1) {
				min = i - start + 1;
				result = s.substr(start, i - start + 1);
			}
			if (count.count(s[start]))
				count[s[start]]++;
			start++;
		}
	}
	return result;
}

018 有效的回文

  • 双指针,指向头和尾,
    若相等,向中间移动,
    若不相等,不是回文串
    不是大小写字母和数字则移动指针,i指针始终要小于j
  • 这里用到了两个c++库函数
    isalnum(char a)判断是否是字母或十进制数字, #include<cctype>
    tolower/toupper(char a)字母转小写/大写, #include<cctype>
bool isPalindrome(string s) {
	int i= 0, j = s.length()-1;
	while (i < j) {
		while (i < j && !isalnum(s[i])) 
			i++;
		while (i<j && !isalnum(s[j])) 
			j--;
		if (tolower(s[i]) != tolower(s[j]))
			return false;
		else {
			i++;
			j--;
		}
	}
	return true;
}

019 最多删除一个字符得到回文

同样设置头尾指针:
若相等,向中间靠拢
若不相等,那么可能的结果一定是从这两个中间删掉一个
判断两种情况是否能得到回文串(删掉后判断剩下的是不是回文串) ,可以结合上一题,写一个判断i到j是否为回文串的函数来调用

bool iTOj(string s, int i, int j) {
	while (i < j) {
		if (s[i] != s[j])
			return false;
		else {
			++i;
			--j;
		}
	}
	return true;
}
bool validPalindrome(string s) {
	int i = 0, j = s.length() - 1;
	if (iTOj(s,0,s.length())) return true;
	while (i < j) {
		if (s[i] == s[j]) {
			++i;
			--j;
		}
		else {
			if (iTOj(s, i + 1, j) || iTOj(s, i, j - 1)) return true;
			else return false;
		}
	}
	return true;
}

020 回文子字符串的个数

这道题要统计回文子串个数,也就是每个字符作为中心的回文串总和
要注意的是回文串长度有可能是奇数也有可能是偶数
为了避免重复,在对每个字符是s[i]做统计时,统计以s[i]为中心和s[i] s[i+1]为中心两种情况总和,需要一个函数来统计

int countCenter(string s, int i, int j) {
	int count = 0;
	while (i >= 0 && j <= s.length() - 1) {
		if (s[i] != s[j])
			return count;
		++count;
		--i;
		++j;
	}
	return count;
}
int countSubstrings(string s) {
	int result = 0;
	for (int i = 0; i < s.length(); i++)
	{
		result += countCenter(s, i, i);
		result+= countCenter(s, i, i+1);
	}
	return result;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值