普通技巧题
LeetCode 31. 下一个排列
2024.05.15 一刷
思路:https://leetcode.cn/problems/next-permutation/solutions/80560/xia-yi-ge-pai-lie-suan-fa-xiang-jie-si-lu-tui-dao-/?envType=study-plan-v2&envId=top-100-liked
代码如下:
class Solution {
public void nextPermutation(int[] nums) {
int len = nums.length;
for (int i = len - 1; i > 0; i--) {
if (nums[i] > nums[i - 1]) {
Arrays.sort(nums, i, len);
for (int j = i; j <len; j++) {
if (nums[j] > nums[i - 1]) {
int temp = nums[j];
nums[j] = nums[i - 1];
nums[i - 1] = temp;
return;// 找到下一个更大的序列,直接返回
}
}
}
}
// 如果不存在下一个更大的排列,则将数字升序排列
Arrays.sort(nums);
return;
}
}
LeetCode 287. 寻找重复数
2024.05.16 二刷
思路:
数组长度为n+1,数字大小范围在1~n,所以至少有一个数重复,题目指定只有一个重复的数;
也就是这个数可能重复好多次,而其它数字只会在数组中出现一次。
标记法:
每次遍历到一个数,将它的值作为下标(index),找到这个下标对应数组中的位置;
我们需要把这个位置上的数(nums[index])做一个标记,表示下标对应的值已经出现过一次了;
可以将nums[index]转为对应的相反数;
那么后面如果再遍历到相同的值,将值作为下标,找到在数组中的位置,查看这个位置上的数是不是负数;
如果是负数,说明这个值之前已经出现过一次了,那就返回下标;
如过不是负数,就正常将值作为下标,找到数组中对应位置,将这个位置的值改为负数;
需要特别注意的是,由于会修改nums[index]为负数,而后面还有可能会遍历到这个负数,
需要根据这个负数作为下标去判断数组中的对应位置的值,就会出现数组越界异常,
所以每次遍历到一个数的时候,先将这个数进行绝对值转换,避免数组越界;
代码如下:
class Solution {
public int findDuplicate(int[] nums) {
int len = nums.length;
for(int num : nums){
int index = Math.abs(num);
if(nums[index]<0){
return index;
}
nums[index] = -nums[index];
}
return len;// n+1
}
}
荷兰国旗问题(三色排序)
LeetCode 75. 颜色分类
2024.05.12 一刷
思路:类似快排分区交换元素的思想
用三个指针:p0,i,p2用来进行划分区间,其中:
- [0,p0)范围内都是0元素,也就是p0左边(不包括p0),全是0;
- [p0,i)范围内都是1元素,也就是p0到i范围(不包括i)全是1;
- (p2,len-1]范围内都是2元素,也就是p2右边(不包括p2)到len-1处都是2;
i用来遍历nums数组;
初始化:要保证三个区间一开始都是空区间
- p0=0;
- i=0;
- p2=len-1;
while循环退出条件:当i遍历到p2时,由于p2右边(不包括p2)到len-1处都是2,
i到p2处还需要判断下nums[p2]的情况,所以当i<=p2,都在while内,当i超过p2就退出循环;
代码如下:
class Solution {
public void sortColors(int[] nums) {
int len = nums.length;
if(len==1){
return;
}
int p0=0;
int i=0;
int p2=len-1;
while(i<=p2){
// [0,p0)
// [p0,i)
// (p2,len-1]
if(nums[i]==0){
swap(nums,p0,i);// 将为0的交换到p0左边区域内
p0++;// 因为p0左边(不包括p0)是0,要维持定义
i++;// i需要继续遍历下一个位置
}else if(nums[i]==1){
i++;// 1本来就在中间,不需要调整位置,p0和p2的位置也不需要向右或向左
}else{// nums[i]==2
swap(nums,i,p2);
p2--;
// 这里i不能+1,因为p2位置交换回来的的是什么数并不知道,
// 还需要对交换完的nums[i]进行判断
}
}
}
private void swap(int[] nums,int index1,int index2){
int tmp = nums[index1];
nums[index1] = nums[index2];
nums[index2] = tmp;
}
}
摩尔投票
Boyer-Moore 投票算法主要用于查找数组中出现次数超过一半的主要元素,核心思想是通过消除不同的元素对来找到主要元素,这个算法的时间复杂度为 O(n),其中 n 是数组的长度。
-
初始化两个变量 candidate 和 count,其中 candidate 用于保存候选主要元素,count
用于记录候选主要元素出现的次数。初始时,candidate 可以是任何数组中的元素,而 count 初始化为0。 -
遍历数组中的每个元素:
-
- 如果 count 等于0,将当前元素设置为候选主要元素 candidate,并将 count 设置为1。
-
- 如果 count 不等于0,检查当前元素是否等于 candidate:
-
-
- 如果相等,将count递增1,表示找到了一个与候选主要元素相同的元素。
-
-
-
- 如果不相等,将 count递减1,表示找到了一个与候选主要元素不同的元素。
-
-
在遍历完成后,candidate 变量中存储的元素就是数组中的主要元素。
169. 多数元素
思路:
1.哈希表统计:时间O(n),空间O(n)
- 遍历数组,hashmap统计各数字的数量,遍历hashmap,找出最大的value对应的key;
2.数组统计:时间O(nlogn),空间O(logn)
- 给数组进行排序,数组中间元素一定为众数
3.摩尔投票法:时间O(n),空间O(1)
- 众数计票为+1,非众数计票为-1,所有数字的票数和>0;
- 若数组的前a个数字的票数和=0(众数和非众数抵消了),则数组nums后面的(n−a)个数中一定还是众数占多数;
- 一个很重要的点—>票数和为0的时候:一定是众数和非众数抵消了,那么就不用管前面抵消的部分了。
- 直接假设剩下的数组的第一个数字n1为众数mode(如果第一个数字n1不是众数,出于众数在本题中的定义,在遍历完数组之前,一定会出现计票为0的情况,而剩下的部分肯定还有众数,那么继续将剩下的部分的第一个数字假设为众数)。
- 这样只要不断假设数组剩余部分的第一个数字为众数进行计票,不是众数就一定会出现计票为0的情况,就可以缩小寻找众数的区间;
- 这样最后剩下的区间的第一个元素一定为众数;
代码如下:
class Solution {
public int majorityElement(int[] nums) {
int mode = 0;// 众数
int votes = 0;// 投票计数
for(int num:nums){
// 投票为0,说明前面非众数与众数相互抵消,将剩余区间的第一个数选为众数
if(votes == 0)mode = num;// 或者初始化众数
if(num == mode){
votes++;
}else{
votes--;
}
}
return mode;
}
}
位运算技巧
一、n&(n-1)
n&(n-1)作用:将n的二进制表示中的最低位为1的改为0。
一个简单的例子:
- n = 10100(二进制),最低位的1是10100(加粗下划线处),
- n-1 = 10011 ,为了能够-1,n的二进制位需要向其最低位的1借位,所以n-1会将最低位的1变成0。
可能会有人问:那10011(n-1)不是比10100(n)还多弄出了一个1吗?所以这时候就需要“&”运算的参与了。“n-1”所造成的多出的1,在和“n”相“&”之后,一定会被消除为0,因为其之所以能多出1,一定是因为原先的“n”在对应位置上为0,借位不得而成的1。
- n&(n-1) = 10000
可以看到原本最低位为1的那位变为0。
应用:
1. 求一个数的二进制表示中的1的个数
while(n>0){
count ++;
n&=(n-1);
}
例题:
力扣 191. 位1的个数
代码如下
public class Solution {
// you need to treat n as an unsigned value
public int hammingWeight(int n) {
int count=0;
while(n!=0){
n&=(n-1);
count++;
}
return count;
}
}
AcWing 801. 二进制中1的个数
原题链接
代码如下:
import java.util.Scanner;
public class Main{
public static void main(String[] args){
Scanner sc =new Scanner(System.in);
int n=sc.nextInt();
int[] a=new int[n];
for(int i=0;i<n;i++)a[i]=sc.nextInt();
for(int i=0;i<n;i++){
int count=0;
while(a[i]>0){
a[i]&=a[i]-1;
count++;
}
a[i]=count;
}
for(int i=0;i<n;i++){
System.out.print(a[i]+" ");
}
}
}
2. 判断一个数是否是2的方幂
n > 0 && ((n & (n - 1)) == 0 )
解释((n & (n-1)) == 0):
如果A&B==0,表示A与B的二进制形式没有在同一个位置都为1的时候。
那么本题到底啥意思??
不妨先看下n-1是什么意思。
令:n=1101011000(二进制,十进制也一样),则
n-1=1101010111。
n&(n-1)=1101010000
由此可以得出,n和n-1的低位不一样,直到有个转折点,就是借位的那个点,从这个点开始的高位,n和n-1都一样,如果高位一样这就造成一个问题,就是n和n-1在相同的位上可能会有同一个1,从而使((n & (n-1)) != 0),如果想要
((n & (n-1)) == 0),则高位必须全为0,这样就没有相同的1。
所以n是2的幂或0。
二、n&(~n+1)或n&-n
n&-n或n&(~n+1)的作用: 保留二进制下最后出现1的位置的数字,其余位置置0;
例题:
AcWing 801. 二进制中1的个数
原题链接
代码如下:
import java.util.Scanner;
public class Main{
// lowbit函数只保留n的二进制最低位的1
public static int lowbit(int n){
return n&-n;
//return n&((~n)+1);
}
public static void main(String[] args){
Scanner sc =new Scanner(System.in);
int n=sc.nextInt();
int[] a=new int[n];
for(int i=0;i<n;i++)a[i]=sc.nextInt();
for(int i=0;i<n;i++){
int count=0;
while(a[i]>0){
a[i]-=lowbit(a[i]);//把二进制最低位的1减去,能减多少次,其二进制就有多少个1
count++;
}
a[i]=count;
}
for(int i=0;i<n;i++){
System.out.print(a[i]+" ");
}
}
}
三、n>>k&1
n>>k&1的作用:可以算出n的二进制表示中,从低到高第k位是0还是1(最低位为第0位)。
举例:n=10,二进制表示:1010,假设求第3位(从0开始)是多少?
n>>3=0001,0001&1=1,所以第三位是1(1010)
四、 ^ 操作(异或)
1.交换两个数
原理:
- a^a=0;
- a^0=a;
- a^b = b^a;交换律
- a ^b ^c =a^ (b^ c)=(a ^b )^ c;结合律
- 所以a^ b^a=b。
LeetCode 344. 反转字符串
对字符数组内容进行翻转,左右指针向中间遍历,交换首尾指针指向的字符。交换采用异或操作,可以不使用额外变量。
代码如下:
class Solution {
public void reverseString(char[] s) {
int l=0,r=s.length-1;
while(l<r){
s[l]^=s[r];//s[l]=s[l]^s[r]
s[r]^=s[l];//s[r]=s[r]^s[l]=s[r]^(s[l]^s[r])=s[l]
s[l]^=s[r];//s[l]=s[l]^s[r]=(s[l]^s[r])^s[l]=s[r]
l++;
r--;
}
}
}
LeetCode 189. 轮转数组
2023.06.03 二刷
题解区里看到有人引用国外的一个短小精悍的题解:
示例:
nums = “----->–>”; k =3
result = “–>----->”;
过程
reverse “----->–>” we can get “<–<-----”
reverse “<–” we can get “–><-----”
reverse “<-----” we can get “–>----->”
代码如下:
class Solution {
public void rotate(int[] nums, int k) {
int n=nums.length;
k%=n;//k可能比nums大,但是nums右移n位还是原来的nums
reverse(nums,0,n-1);//反转区间两端都为闭
reverse(nums,0,k-1);
reverse(nums,k,n-1);
}
//对数组指定区间进行反转
public void reverse(int[] nums,int l,int r ){
while(l<r){
// 基于异或运算的交换律和结合律,以及a^a=0,a^0=a;
nums[l]^=nums[r];//nums[l]=nums[l]^nums[r]
nums[r]^=nums[l];//nums[r]=nums[r]^nums[l]^nums[r]=nums[l]
nums[l]^=nums[r];//nums[l]=(nums[l]^nums[r])^nums[l]=nums[r]
l++;
r--;
}
}
}
2.找到只出现一次的数
LeetCode 136. 只出现一次的数字
给你一个 非空 整数数组 nums ,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。
2024.05.10 二刷
思路:异或运算
需要知道几个异或的性质:
- 任何数和 0 做异或运算,结果仍然是原来的数,比如a^0=a;
- 任何数和其自身做异或运算,结果是0,比如a^a=0;
- 异或运算满足交换律和结合律,比如aba=baa=b(aa)=b^0=b;
知道这几个性质就很容易想到方法了:
- 只需要最开始定义一个res=0,然后把nums中每一个数都和res进行异或,
- 出现两次的数在异或之后,采用交换律和结合律,会变成0,
- 0和原来的res=0异或还是0,那么只出现一次的那个数和res异或之后就是那个只出现一次的数了。
代码如下:
class Solution {
public int singleNumber(int[] nums) {
int res=0;
for(int num:nums){
res ^= num;
}
return res;
}
}