对于自己刷题过程中遇到的一些常见简单解题方法进行了一个总结:
数组在数据结构中是线性表的一种,在算法题中常常以整数数组和字符串等形式展现,其实数组中包含有更多的数据类型,这一段主要说明整数数组的一些常见问题解法;
数组的一个特点,可以通过下标对于数据进行一个快速访问,即a[i] = xx;
位运算
本质上应该算在数学系算法的一大类,通过数组中的各个数进行一个位运算来获得最终的结果。
位运算指数转化为二进制后的一系列运算,比如与、或、非、同或、异或、左移右移等,通过这些运算达到一个高效率的算法;
常见的是利用它的一些特性来解决问题:比如左右移位的按位处理;异或结果的两个运算数相同为0,不同为1;
以一些例题说明:
这一题要求实现两个数相除;不允许使用除号、称号、取余;
解法:分析问题直接从结果来看需要得到除法,假设计算A/B,除法本质是数A中包含少个B;
那么一种直观的解法是采用减法循环实现,就用 A=A-B这种形式,直到无法再减;比如10/3;它可以减3次,到第三次A等于1无法再减了;总共循环了三次,所以结果为3;
接受两个参数A\B,除数与被除数
define count = 0;计数器;
while(A大于B){
A-B,;计数器加1;
}
返回计数器;
(这里没有考虑越界情况,仅仅解释算法)
这种算法显然有待优化,优化的方式是 每次尝试去减B的倍数,尝试一次多记几次计数器;比如13/3;尝试13/(3+3+3+3)= 1;实际结果应该是这四个三相加; 不能使用乘法;所以这里可以使用左移,即3<<<4;可以直接得到结果而不用再加;因此可以用高位来慢慢向下移动直到找到结果;
接受参数A和B;
define 计数器
for(i=31 to i=0){
如果B左移 i 次的结果大于A;那么计数器等于这个值;
}
return 计数器
2、leetcode137只出现一次的数字
这一题是指一个数组中只有一个数出现一次,其他出现3次;求这个数,比如[2,2,1,2,3,3,3]的结果为1;
解法:以数字的二进制形式来说,对于出现三次的数字,二进制位出现的次数都是 3 的倍数。
因此,统计所有数字的各二进制位中1的出现次数,并对 3 求余,结果则为只出现一次的数字。
举个例子 比如 [1,1,1,3];每一个数的每一个二进制位都加起来然后取余3,得到的结果应该是011即3;
//循环32次,int有32位
for(int i = 0;i<32;i++){
int sum = 0;
for(遍历数组中的数n){
//对数组中的每一个数,每次右移 i 位在与1;即取出第i位上的值;
sum += (n>>i)&1;
}
//对每一二进制位上的数求和取余得到最终结果的第i位上的值;在归位;
sum%=3;
ans+=sum<<i;
}
return ans;
}
3、leetcode318求最大单词长度
给定一个字符串数组 words,找到 length(word[i]) * length(word[j]) 的最大值,并且这两个单词不含有公共字母。你可以认为每个单词只包含小写字母。如果不存在这样的两个单词,返回 0。
即 数组中两个完全不包含重复字母的单词最长结果;
解法:不同单词包含不同的字母,可以直接想到为每个单词建立一个字典记录他们的各个字母;因此可以使用二进制来标识他们各个字母的出现情况,“ac”标识为101;而“ad”标识为1001;“abc”表示为111;这样就可以把每个单词表示开来;最后在遍历数组时比较二进制位即可;
标识过程:
定义记录数组;
for(遍历整个数组){
for(遍历该位置上的字符串数组){
以ascll表为例,用记录数组记录1右移每个字符编码减去a位置上的编码的位数;
}
}
检测过程
for(遍历记录数组){
for(遍历当前下标后边的记录数组){
比较单词上的二进制制位是否相同;如果相同则记录最大值;
}
}
双指针
双指针从广义上来说,是指用两个变量在线性结构上遍历而解决的问题。在数组中,可以是同向或是双向的;在链表里,两个指针为同向移动,但速率可以不同,一般叫做快慢指针;
双指针的一些常见应用场景:滑动窗口、排序、二分查找等;
分类:同向指针:快慢、间隔指针; 异向指针:对撞;
使用时需要注意的问题:指针指向,移动方向,起始位置,何时移动、移动速率;
1、对撞指针的应用;
对撞指针一般指两个不同方向移动的指针,比如一个从头向尾,一个从尾向头移动;典型的一个对撞指针应用是二分查找的算法;二分查找一般的使用方式是这样
定义左指针 = 0;
右指针等于长度 - 1;
while(左指针小于右指针){
程序操作;
if(条件){
左++;
}
if(条件){
右--;
}
}
例题
lc15三数之和
给一个数组,求数组中的三元组[a,b,c]使得a+b+c=0;
解法:这题本质还是一个查找的算法;如果我们知道"a",则在数组里找" -(b+c) ";
使用排序加+对撞指针来做
数组排序
查找结果
定义结果集;
for (从第一个数到倒数第3个数) {
if(i!=0&&nums[i]==nums[i-1]) continue;
定义左右指针,左指针等于外循环的i,右指针为数组末尾;
while(left<right){
跳过重复的值;防止记录重复结果;
记录左右指针处的数的和sum;
if(如果sum刚好等于当前外循环数a的相反数){
记录左右指针结果;并移动左右指针到值不重复的地方;
}else if( 这个左右指针数的和比外循环的值的相反数小){
左指针移动eft++;
}else{ //这个和比外循环的值的相反数大;
右指针移动right--;
}
}
}
最后返回结果集即可;
2、同向快慢指针;左右指针都从一侧向另一侧移动;经典案例是链表的是否有环;
通过速度不一致的两个指针,一前一后一起移动,直到指针重合在一起;
代码:
public boolean hasCycle(ListNode head) {
ListNode slow = head;
ListNode fast = head;
while (slow != null && fast != null) {
ListNode n = fast.next;
fast = n == null ? null : n.next;
if (slow == fast) {
return true;
}
slow = slow.next;
}
return false;
}
3、同向间隔指针;类似于滑动窗口,一个指针在前,一个指针后面,同步或者不同步移动来解决某些问题;即定义一个左指针在左边移动,右指针在后面移动;
判断字符串中没有字符的最长字串;
解法:通过滑动窗口移动解决,左指针不移动,右指针向前移动;当右指针移动到重复的字符时,将左指针拉到右指针处,记录最大值;
public int lengthOfLongestSubstring(String s) {
if (s.length()==0) return 0;
HashMap<Character, Integer> map = new HashMap<Character, Integer>();
int max = 0;
int left = 0;
for(int i = 0; i < s.length(); i ++){
if(map.containsKey(s.charAt(i))){
left = Math.max(left,map.get(s.charAt(i)) + 1);
}
map.put(s.charAt(i),i);
max = Math.max(max,i-left+1);
}
return max;
}
前缀和
通过预先处理数组来获得结果,前缀和是指记录数组的前n项和;比如数组[1,2,3,4],即在新数组中存储结果[1,3,6,10];通过这样的数组来解决某些问题;
通过前缀和可以很容易的求到子数组的n项和,而通过子数组的和可以解决很多求和类问题,比如求数组中间数使前面和等于后边的和,或者和大于某数的子数组等;利用这个思想也可以解决乘积相关的问题;
lc560和为k的子数组
给你一个整数数组 nums 和一个整数 k ,请你统计并返回该数组中和为 k 的连续子数组的个数。
public int subarraySum(int[] nums, int k) {
int count = 0;
//前缀和, 使用hash表来记录前缀和,如果在后续的遍历中存在有sum-k, 即当前前缀和-k = 以前前缀和,则代表从该位置到现在这个位置的和等于k
HashMap<Integer,Integer> map = new HashMap<>();
map.put(0,1);
int sum = 0;
for(int i = 0;i<nums.length;i++){
sum+=nums[i];
count+=map.getOrDefault(sum-k,0);
map.put(sum,map.getOrDefault(sum,0)+1);
}
return count;
}
差分数组
与前缀和类似,差分数组是指对原数组进行差分运算得到的新数组,即[1,2,3,4]的到的新数组为[1,-1,-2,-3];利用它也可以解决很多类似的问题;