剑指offer刷题(算法类_2) 排序 位运算 其他算法

035-数组中的逆序对(归并排序)

题目描述

题解

代码

复杂度

050 数组中重复的数字

题目描述

在一个长度为 n 的数组nums里的所有数字都在 0~n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每个数字重复了几次。请找出数组中任意一个重复的数字。

示例 1:
输入:[2, 3, 1, 0, 2, 5, 3]
输出:2 或 3

限制:

  • 2 <= n <= 100000

题解

方法一:HashSet

方法二:原地置换
如果没有重复数字,那么正常排序后,数字i应该在下标为i的位置,所以思路是重头扫描数组,遇到下标为i的数字如果不是i的话,(假设为m),那么我们就拿与下标m的数字交换。在交换过程中,如果有重复的数字发生,那么终止返回ture

代码

HashSet

class Solution {
    public int findRepeatNumber(int[] nums) {
            HashSet<Integer> set = new HashSet<>();
            for(int num : nums){
                  if(set.contains(num)){
                        return num;
                  }else{
                        set.add(num);
                  }
            }
            return -1;
      }
}

原地置换

class Solution {
    public int findRepeatNumber(int[] nums) {
        for(int i=0;i<nums.length;i++){
            while (nums[i]!=i){
                if(nums[i]==nums[nums[i]]){
                    return nums[i];
                }
                int temp=nums[i];
                nums[i]=nums[temp];
                nums[temp]=temp;
            }
        }
        return -1;
    }
}

复杂度

HashSet

  • 时间复杂度: O(N)
  • 空间复杂度 O(N)

原地置换

  • 时间复杂度: O(N)
  • 空间复杂度: O(1)

029-最小的K个数(堆排序)

题目描述

题解

代码

复杂度

029-最小的K个数(快速排序)

题目描述

题解

代码

复杂度

位运算

011-二进制中1的个数

题目描述

输入一个整数 n ,求1~n这n个整数的十进制表示中1出现的次数。

例如,输入12,1~12这些整数中包含1 的数字有1、10、11和12,1一共出现了5次。

示例 1:

输入:n = 12
输出:5

示例 2:

输入:n = 13
输出:6

限制:

1 <= n < 2^31

题解

解法一 暴力

直接想到的当然就是暴力的方法,一个数一个数的判断,一位一位的判断。

public int countDigitOne(int n) {
    int num = 0;
    for (int i = 1; i <= n; i++) {
        int temp = i;
        while (temp > 0) {
            if (temp % 10 == 1) {
                num++;
            }
            temp /= 10;
        }
    }
    return num;
}

但这个解法会超时。

解法二
自己也没想到别的方法,讲一下这里的思路。

总体思想就是分类,先求所有数中个位是 1 的个数,再求十位是 1 的个数,再求百位是 1 的个数...

假设 n = xyzdabc,此时我们求千位是 1 的个数,也就是 d 所在的位置。

那么此时有三种情况,

d == 0,那么千位上 1 的个数就是 xyz * 1000
d == 1,那么千位上 1 的个数就是 xyz * 1000 + abc + 1
d > 1,那么千位上 1 的个数就是 xyz * 1000 + 1000

为什么呢?

当我们考虑千位是 1 的时候,我们将千位定为 1,也就是 xyz1abc

对于 xyz 的话,可以取 0,1,2...(xyz-1),也就是 xyz 种可能。

xyz 固定为上边其中的一个数的时候,abc 可以取 0,1,2...999,也就是 1000 种可能。

这样的话,总共就是 xyz*1000 种可能。

注意到,我们前三位只取到了 xyz-1,那么如果取 xyz 呢?

此时就出现了上边的三种情况,取决于 d 的值。

d == 1 的时候,千位刚好是 1,此时 abc 可以取的值就是 0 到 abc ,所以多加了 abc + 1。

d > 1 的时候,d 如果取 1,那么 abc 就可以取 0 到 999,此时就多加了 1000。

再看一个具体的例子。

如果n = 4560234
让我们统计一下千位有多少个 1
xyz 可以取 0 到 455, abc 可以取 0 到 999
4551000 to 4551999 (1000)
4541000 to 4541999 (1000)
4531000 to 4531999 (1000)
...
  21000 to   21999 (1000)
  11000 to   11999 (1000)    
   1000 to    1999 (1000)
总共就是 456 * 1000

如果 n = 4561234
xyz 可以取 0 到 455, abc 可以取 0 到 999
4551000 to 4551999 (1000)
4541000 to 4541999 (1000)
4531000 to 4531999 (1000)
...
1000 to 1999 (1000)
xyz 还可以取 456, abc 可以取 0 到 234
4561000 to 4561234 (234 + 1)
总共就是 456 * 1000 + 234 + 1

如果 n = 4563234
xyz 可以取 0 到 455, abc 可以取 0 到 999    
4551000 to 4551999 (1000)
4541000 to 4541999 (1000)
4531000 to 4531999 (1000)
...
1000 to 1999 (1000)
xyz 还可以取 456, abc 可以取 0 到 999
4561000 to 4561999 (1000)
总共就是 456 * 1000 + 1000

至于其它位的话是一样的道理。

总结

将 1 ~ n 的个位、十位、百位、...的 1 出现次数相加,即为 1 出现的总次数。

设 n = xyzdabc,

  • 称 " d" 为 当前位 ,记为 cur

  • 将 "abc" 称为 低位 ,记为 low

  • 将 " xyz " 称为 高位 ,记为 high

  • 将 10^i 称为 位因子 ,记为 digit

某位中 1 出现次数的计算方法:

根据当前位 cur 值的不同,分为以下三种情况:

  1. cur = 0 时: 此位 1 的出现次数只由高位 high 决定,计算公式为:high x dight

  2. cur = 1 时: 此位 1 的出现次数由高位high 和低位 low决定,计算公式为: high x dight + low + 1

  3. cur > 1 时: 此位 1 的出现次数只由高位 high 决定,计算公式为:high x dight + dight

代码

class solution{
      public int countDigitOne(int n) {
            int dight = 1,res = 0;
            int hight = n / 10, cur = n % 10, low = 0;
            while(high != 0 || cur != 0){
                  if(cur == 0) res += high * dight;
                  else if(cur == 1) res += hight * dight + low + 1;
                  else res += hight * dight + dight;
                  low += cur * dight;
                  cur = hight % 10;
                  hight /= 10;
                  dight *= 10;
             }
             return res;
      }
}

复杂度

  • 时间复杂度

  • 空间复杂度

012-数值的整数次方

题目描述

一个整型数组里除了两个数字之外,其他的数字都出现了两次。请写程序找出这两个只出现一次的数字。

题解

=方法一:哈希法
使用HashMap

方法二:位运算

前提知识:

异或运算:两个相同数字异或 = 0,一个数和0异或还是它本身。

当只有一个数出现一次时,我们把数组中所有的数,依次异或运算,最后剩下的就是落单的数,因为成对儿出现的都抵消了。

代码

方法一:哈希法

//num1,num2分别为长度为1的数组。传出参数
//将num1[0],num2[0]设置为返回结果
import java.util.HashMap;
public class Solution {
    public void FindNumsAppearOnce(int [] array,int num1[] , int num2[]) {
        //HashMap
        HashMap<Integer,Integer> map = new HashMap<>();
        for(int arr : array){
            if(map.containsKey(arr)){
                map.put(arr,2);
            }else{
                map.put(arr,1);
            }
        }
        
        int count = 0;
        for(int i=0; i < array.length; i++){
            if(map.get(array[i]) == 1){
                if(count == 0){
                    num1[0] =  array[i];
                    count++;
                }else
                    num2[0] =  array[i];
            }
        }
    }
}    

方法二:位运算


public void FindNumsAppearOnce(int [] array,int num1[] , int num2[]) {

        int xor1 = 0;
        for(int i=0; i < array.length; i++)
            xor1 = xor1^array[i];
        //在xor1中找到第一个不同的位对数据进行分类,分类为两个队列对数据进行异或求和找到我们想要的结果
        int index = 1;
        while((index & xor1)==0)
            index = index <<1;//因为可能有多个位为1所以需要求一下位置
        int result1 = 0;
        int result2 = 0;
        for(int i=0; i < array.length; i++){
            if((index & array[i]) == 0)
                result1 = result1^array[i];
            else
                result2 = result2^array[i];
        }
        num1[0] = result1;
        num2[0] = result2;
}

复杂度

法一:哈希法

  • 时间复杂度:O(n)
  • 空间复杂度:O(1)

方法二:位运算

  • 时间复杂度:O(n)
  • 空间复杂度:O(1)

040-数组中只出现一次的数字

题目描述

题解

代码

复杂度

不用加减乘除做加法

题目描述

写一个函数,求两个整数之和,要求在函数体内不得使用 “+”、“-”、“*”、“/” 四则运算符号。

示例:
输入: a = 1, b = 1
输出: 2

提示:

  • a, b 均可能是负数或 0
  • 结果不会溢出 32 位整数

题解

本题考察对位运算的灵活使用,即使用位运算实现加法。

设两数字的二进制形式 a, b ,其求和 s = a + b ,a(i) 代表 a 的二进制第 i 位,则分为以下四种情况:

剑指offer刷题(算法类_2)
排序
位运算
其他算法

观察发现,无进位和异或运算 规律相同,进位与运算 规律相同(并需左移一位)。因此,无进位和 nn 与进位 cc 的计算公式如下:
剑指offer刷题(算法类_2)
排序
位运算
其他算法

  • n = a ^ b ;非进位和,异或运算
  • c = a & b << 1; 进位:与运算 + 左移一位
  • (和s)= (非进位和n)+ (进位c)

循环求 n 和 c ,直至进位 c = 0 ;此时 s = n ,返回 n 即可。

Q : 若数字 aa 和 bb 中有负数,则变成了减法,如何处理?
A : 在计算机系统中,数值一律用 补码 来表示和存储。补码的优势: 加法、减法可以统一处理(CPU只有加法器)。因此,以上方法 同时适用于正数和负数的加法

代码

class Solution{
    public int add(int a, int b){
        while(b != 0){
            int c = (a & b) << 1;
            a ^= b;
            b = c;
        }
        return a;
    }
}

复杂度

  • 时间复杂度 O(1) : 最差情况下(例如 a = 0x7fffffff ,b=1 时),需循环 32 次,使用 O(1) 时间;每轮中的常数次位操作使用 O(1) 时间。
  • 空间复杂度 O(1) : 使用常数大小的额外空间。

其他算法

题目描述

题解

代码

复杂度

002-替换空格

题目描述

题解

代码

复杂度

013-调整数组顺序使奇数位于偶数前面

题目描述

题解

代码

复杂度

028-数组中出现次数超过一半的数字

题目描述

数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。例如输入一个长度为9的数组{1,2,3,2,2,2,5,4,2}。由于数字2在数组中出现了5次,超过数组长度的一半,因此输出2。如果不存在则输出0。

示例1

输入
[1,2,3,2,2,2,5,4,2]

返回值
2

题解

1、哈希表统计法:找出数组中超过一半的数,可用用HashMap<num,出现的次数>来判断,只要map中的value > length / 2,返回map的key即可。

2、数组排序法: 将数组 nums 排序,数组中点的元素 一定为众数。

3、摩尔投票法: 核心理念为 票数正负抵消 。此方法时间和空间复杂度分别为 O(N) 和 O(1) ,为本题的最佳解法

此题中这个众数不一定存在,对于法 1 和法 2 还需要遍历一遍数组判断是否为众数,

代码

//摩尔投票法
public class Solution {
    public int MoreThanHalfNum_Solution(int [] array) {
        int value = 0, count = 0;
        for(int num : array){
            if(count == 0) value = num;
            count += value == num ? 1 : -1;
        }
        int isNum = 0;
        int res;
        for(int num : array){
            isNum += value == num ? 1 : -1;
        }
        return res = isNum > 0 ? value : 0;
    }
}

复杂度

  • 时间复杂度:O (N)
  • 空间复杂度:O (1)

031-整数中1出现的次数(从1到n整数中1出现的次数)

题目描述

题解

代码

复杂度

032-把数组排成最小的数

题目描述

输入一个正整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。

例如
输入数组 {3,32,321}
则打印出这三个数字能排成的最小数字为321323

题解

比较两个字符串s1, s2大小的时候,先将它们拼接起来,比较 s1+s2,和s2+s1那个大,如果s1+s2大,那说明s2应该放前面,所以按这个规则,s2就应该排在s1前面。

代码

复杂度

033-丑数

题目描述

题解

代码

复杂度

041-和为S的连续正数序列(滑动窗口思想)

题目描述

输入一个正整数 target ,输出所有和为 target 的连续正整数序列(至少含有两个数)。

序列内的数字由小到大排列,不同序列按照首个数字从小到大排列。

示例 1:
输入:target = 9

输出:[[2,3,4],[4,5]]

示例 2:
输入:target = 15
输出:[[1,2,3,4,5],[4,5,6],[7,8]]
 
限制:
1 <= target <= 10^5

题解

代码·

public int[][] findContinuousSequence(int target) {
    int i = 1; // 滑动窗口的左边界
    int j = 1; // 滑动窗口的右边界
    int sum = 0; // 滑动窗口中数字的和
    List<int[]> res = new ArrayList<>();

    while (i <= target / 2) {
        if (sum < target) {
            // 右边界向右移动
            sum += j;
            j++;
        } else if (sum > target) {
            // 左边界向右移动
            sum -= i;
            i++;
        } else {
            // 记录结果
            int[] arr = new int[j-i];
            for (int k = i; k < j; k++) {
                arr[k-i] = k;
            }
            res.add(arr);
            // 左边界向右移动
            sum -= i;
            i++;
        }
    }

    return res.toArray(new int[res.size()][]);
}


复杂度

042-和为S的两个数字

输入一个正整数 target ,输出所有和为 target 的连续正整数序列(至少含有两个数)。

序列内的数字由小到大排列,不同序列按照首个数字从小到大排列。

题目描述

示例 1:
输入:target = 9
输出:[[2,3,4],[4,5]]

示例 2:
输入:target = 15
输出:[[1,2,3,4,5],[4,5,6],[7,8]]

限制:
1 <= target <= 10^5

题解

方法一:双指针/ 滑动窗口法

我们用两个指针 l 和 r 表示当前枚举到的以 l 为起点到 r 的区间,sum 表示 [l,r] 的区间和,由求和公式可 O(1) 求得为 sum = (l + r)x (r - l + 1) / 2 ,起始 l=1,r=2。

一共有三种情况:

  1. 如果 sum < target 则说明指针 r 还可以向右拓展使得 sum 增大,此时指针 r 向右移动,即 r += 1;
  2. 如果 sum>target 则说明以 l 为起点不存在一个 r 使得 sum=target ,此时要枚举下一个起点,指针 l 向右移动,即l += 1
  3. 如果 sum==target 则说明我们找到了以 l 为起点得合法解 [l,r] ,我们需要将 [l,r] 的序列放进答案数组,且我们知道以 l 为起点的合法解最多只有一个,所以需要枚举下一个起点,指针 l 向右移动,即 l += 1
    终止条件即为 l>=r的时候,这种情况的发生指针 r 移动到了 target的位置,导致 l<r 的时候区间和始终大于 target 。

代码

class Solution {
    public int[][] findContinuousSequence(int target) {
        List<int[]> vec = new ArrayList<int[]>();
        for (int l = 1, r = 2; l < r;) {
            int sum = (l + r) * (r - l + 1) / 2;
            if (sum == target) {
                int[] res = new int[r - l + 1];
                for (int i = l; i <= r; ++i) {
                    res[i - l] = i;
                }
                vec.add(res);
                l++;
            } else if (sum < target) {
                r++;
            } else {
                l++;
            }
        }
        return vec.toArray(new int[vec.size()][]);
    }
}

复杂度

  • 时间复杂度:由于两个指针移动均单调不减,且最多移动 target / 2 次,即方法一提到的枚举的上界,所以时间复杂度为 O(target) 。

  • 空间复杂度:O(1) ,除了答案数组只需要常数的空间存放若干变量。

043-左旋转字符串

题目描述

字符串的左旋转操作是把字符串前面的若干个字符转移到字符串的尾部。请定义一个函数实现字符串左旋转操作的功能。比如,输入字符串" abcdefg"和数字2,该函数将返回左旋转两位得到的结果"cdefgab"。

题解

方法一:遍历数组
分别遍历待翻转数组的[n,arrar.length-1] [0, n-1]

方法二:三次翻转
假设输入str为abcXYZdef,n=3

  1. 反转前n个字符,得到cbaXYZdef
  2. 反转第n个字符后面所有的字符cbafedZYX
  3. 反转整个字符串XYZdefabc

代码

遍历拼接

public class Solution {
    public String LeftRotateString(String str,int n) {
        if(str.length() == 0) return str;
        StringBuffer res = new StringBuffer();
        for(int i = n; i < str.length(); i++){
            res.append(str.charAt(i));
        }
        for(int i = 0; i < n; i++){
            res.append(str.charAt(i));
        }
        return res.toString();
    }
}

利用求余运算,可以简化代码。

public class Solution {
    public String LeftRotateString(String str,int n) {
        if(str.length() == 0) return str;
        StringBuffer res = new StringBuffer();
        for(int i = n; i < str.length() + n; i++){
            res.append(str.charAt(i % str.length()));
        }
        return res.toString();
    }
}

三次翻转

public class Solution {
    public String LeftRotateString(String str,int n) {
        if(str.length() == 0) return str;
        char[] ch = str.toCharArray();
        Reverse(ch, 0, n-1);
        Reverse(ch, n, ch.length - 1);
        Reverse(ch,0, ch.length - 1);
        return String.valueOf(ch);
    }
    
    void Reverse(char[] ch, int l, int r){
        while(l < r){
            char temp = ch[l];
            ch[l++] = ch[r];
            ch[r--] = temp;
        }
    }
}

String.valueOf()方法: 这个方法是静态的,直接通过String调用

复杂度

方法一:遍历法

  • 时间复杂度: O(N)
  • 空间复杂苏: O(N)

方法一:翻转法

  • 时间复杂度: O(N)
  • 空间复杂苏: O(1)

046-孩子们的游戏-圆圈中最后剩下的数(约瑟夫环)

题目描述

0,1,,n-1这n个数字排成一个圆圈,从数字0开始,每次从这个圆圈里删除第m个数字。求出这个圆圈里剩下的最后一个数字。

例如,0、1、2、3、4这5个数字组成一个圆圈,从数字0开始每次删除第3个数字,则删除的前4个数字依次是2、0、4、1,因此最后剩下的数字是3

示例 1:

输入: n = 5, m = 3
输出: 3

示例 2:

输入: n = 10, m = 17
输出: 2

限制:

  • 1 <= n <= 10^5
  • 1 <= m <= 10^6

题解

约瑟夫问题比较难想的点有两个:

  1. 当数到最后一个结点不足m个时,需要跳到第一个结点继续数。
  2. 每轮都是上一轮被删结点的下一个结点开始数 m 个。

第一点比较好解决,可以通过取余来完成。
第二点的解决方案是:将删除结点的后继作为下一轮的第一个结点,后续结点依次排列。这样每轮都是从首结点开始数 m 个了

设函数 f(n,m) 输出最后结点的编号,结点编号从 0 开始,n 为结点个数,m 为删除步长。
n = 1:f(n,m) = 0 ,n =1; n > 1;f(n,m) = (f(n-1,m) + m) % n; ​

代码

public Solution{
      public int lastRemaining(int n, int m){
            if(n == 0) return -1;
            //最后一个人的位置初始位置 0
            int res = 0;
            //最后一轮剩下两人,从2开始反推
            for(int i = 2; i <= n; i++){
                  res = (res + m) % i;
            }
            return res;
      }
}

复杂度

  • 时间复杂度:O(N)
  • 空间复杂度:O(1)

051-构建乘积数组

题目描述

给定一个数组 A[0,1,…,n-1],请构建一个数组 B[0,1,…,n-1],其中 B 中的元素 B[i]=A[0]×A[1]×…×A[i-1]×A[i+1]×…×A[n-1]。不能使用除法
 
示例:

输入: [1,2,3,4,5]
输出: [120,60,40,30,24]
 
提示:

  • 所有元素乘积之和不会溢出 32 位整数
  • a.length <= 100000

题解

本题的难点在于 不能使用除法 ,即需要 只用乘法 生成数组 B 。根据题目对B[i] 的定义,可列表格,如下图所示。
剑指offer刷题(算法类_2)
排序
位运算
其他算法

根据表格的主对角线(全为 1 ),可将表格分为 上三角下三角 两部分。分别迭代计算下三角和上三角两部分的乘积,即可 不使用除法 就获得结果。

  • 初始化:数组 B ,其中 B[0] = 1 ;辅助变量 tmp = 1 ;
  • 计算 B[i] 的 下三角 各元素的乘积,直接乘入 B[i] ;
  • 计算 B[i] 的 上三角 各元素的乘积,记为 tmp ,并乘入 B[i] ;
  • 返回 B ;

代码

public class Solution {
    public int[] multiply(int[] A) {
        int[] res = new int[A.length];
        res[0] = 1;
        //下三角计算
        for(int i = 1; i < A.length; i++){
            res[i] = res[i - 1] * A[i - 1];
        } 
        //结果 = 下三角 * 上三角
        int tem = 1;
        for(int i = A.length - 2; i >= 0;i--){
            tem *= A[i + 1];
            res[i] *= tem; 
               
        }
        return res;
    }
}

复杂度

  • 时间复杂度 O(N) : 其中 N 为数组长度,两轮遍历数组 a ,使用 O(N) 时间。
  • 空间复杂度 O(1) : 变量 tmp 使用常数大小额外空间(数组 b 作为返回值,不计入复杂度考虑)。

051-扑克牌中的顺子

题目描述

从扑克牌中随机抽5张牌,判断是不是一个顺子,即这5张牌是不是连续的。2~10为数字本身,A为1,J为11,Q为12,K为13,而大、小王为 0 ,可以看成任意数字。A 不能视为 14。

示例 1:
输入: [1,2,3,4,5]
输出: True

示例 2:
输入: [0,0,1,2,5]
输出: True

题解

根据题意,此 55 张牌是顺子的 充分条件 如下:

  1. 除大小王外,所有牌 无重复 ;
  2. 设此 5 张牌中最大的牌为 max ,最小的牌为 min(大小王除外),则需满足:
  • max - min < 5

因而,可将问题转化为:此 5 张牌是否满足以上两个条件?

方法一: 集合 Set + 遍历

  • 遍历五张牌,遇到大小王(即 0 )直接跳过。

  • 判别重复: 利用 Set 实现遍历判重, Set 的查找方法的时间复杂度为 O(1) ;

  • 获取最大 / 最小的牌: 借助辅助变量 max 和 min ,遍历统计即可。

代码

public class Solution {
    public boolean isContinuous(int [] numbers) {
        if(numbers.length == 0) return false;
        int min = 14, max = 0;
        Set<Integer> set = new HashSet<>();
        for(int num : numbers){
            if(num == 0) continue;
            max = Math.max(max,num);
            min = Math.min(min,num);
            if(set.contains(num)){
                return false;
            }
            set.add(num);
        }
        return (max - min) < 5;
 
    }
}

方法二:排序 + 遍历

  • 先对数组执行排序。
  • 判别重复: 排序数组中的相同元素位置相邻,因此可通过遍历数组,判断 nums[i] = nums[i + 1]是否成立来判重。
  • 获取最大 / 最小的牌: 排序后,数组末位元素 nums[4] 为最大牌;元素 nums[joker] 为最小牌,其中 joker 为大小王的数量。

代码

public class Solution {
    public boolean isContinuous(int [] numbers) {
        if(numbers.length == 0) return false;
        Arrays.sort(numbers);
        int jocker = 0;//大小王的数量
        //遍历数组,判重和统计大小王的数量
        for(int i = 0; i < 4; i++){
            if(numbers[i] == 0) jocker++;
            else if (numbers[i] == numbers[i + 1]) return false;
        }
        return (numbers[4] - numbers[jocker] < 5);
    }
}

复杂度

  • 时间复杂度: O(1)
  • 空间复杂度: O(1)