优选算法 - 3 ( 位运算 && 模拟 && 分治 11000 字详解 )

一: 位运算

1.1 常见位运算总结

请添加图片描述
请添加图片描述

1.2 判断字符串是否唯一

题目链接:判断字符串是否唯一

在这里插入图片描述
请添加图片描述

class Solution {
    public boolean isUnique(String astr) {
        //利用位图思想判断字符是否唯一
        int bitMap = 0;
        //鸽巢原理,如果 astr 的长度大于了 26,那么这个字符串一定是会重复的
        if(astr.length() > 26) return false;

        for(int i = 0; i < astr.length(); i++){
            //先获取一下字符在位图中的位置
            int x = astr.charAt(i) - 'a';
            //判断一下位图的第 i 位是不是等于 1,如果等于 1 说明这个字符已经存入过了
            if(((bitMap >> x) & 1) == 1) return false;
            bitMap |= (1 << x); 
        }
        
        return true;
    }
}

在这里插入图片描述

1.3 丢失的数字

题目链接:丢失的数字

在这里插入图片描述
请添加图片描述

class Solution {
    public int missingNumber(int[] nums) {
        int ret = 0;
        for(int i = 0; i < nums.length; i++) 
            ret ^= nums[i];
        for(int i = 0; i <= nums.length; i++)
            ret ^= i;

        return ret;
    }
}

在这里插入图片描述

1.4 两整数之和

题目链接:两整数之和

在这里插入图片描述
在这里插入图片描述

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

在这里插入图片描述

1.5 只出现一次的数字 II

题目链接:只出现一次的数字 II

在这里插入图片描述

请添加图片描述

class Solution {
    public int singleNumber(int[] nums) {
        //首先定义一个 ret 用于存储结果
        int ret = 0;

        //接着我们开始判断 ret 的每一位是否需要修改成 1
        for(int i = 0; i < 32; i++){
            //用于统计数组中所有数在这一位上 1 的出现次数
            int sum = 0;
            for(int x : nums){
                if(((x >> i) & 1) == 1) sum++;
            }

            //统计完这一位上 1 的出现次数后开始操作
            sum %= 3;
            if(sum == 1) ret |= 1 << i; // 使用位操作将 ret 的第 i 位设置为 1
        }

        //经过对 ret 的 32 位进行判断后, ret 存储的值就是只出现一次的那个数字
        return ret;
    }
}

在这里插入图片描述

1.6 消失的两个数字

题目链接:消失的两个数字

在这里插入图片描述

请添加图片描述
请添加图片描述

class Solution {
    public int[] missingTwo(int[] nums) {
        //用 tmp 存储数组中所有元素和 0 - nums。length + 2 的所有数异或的结果
        int tmp = 0;
        //先异或上数组中的元素
        for(int x : nums)
            tmp ^= x;
        //接着异或 0 - nums。length + 2 中的元素
        for(int i = 1; i <= nums.length + 2; i++)
            tmp ^= i;

        //经过两论异或后,tmp = a ^ b,因为两个相同的数进行异或都抵消掉了,接着我们去找比特位为 1 的最右侧那一位,用 diff 标记
        int diff = 0;
        while(true){
            if(((tmp >> diff) & 1) == 1) break;
            else diff ++;
        }

        //接着我们通过 diff 来把数组中的元素和 0 - nums。length + 2 的元素分为两大类,一大类是 diff 为 0 的类,一类是 diff 为 1 的类,因为两个大类一定存在只有一个元素的(a 或 b),和存在两个相同元素的,两个相同元素异或后会抵消,所以分别把两大类的数异或在一起,就是 a 或者 b 的结果
        //用 ret 存储 a,b 两个数
        int[] ret = new int[2];
        for(int x : nums){
            if(((x >> diff) & 1) == 0) ret[0] ^= x;
            else ret[1] ^= x;           
        }

        for(int i = 1; i <= nums.length + 2; i++){
            if(((i >> diff) & 1) == 0) ret[0] ^= i;
            else ret[1] ^= i;   
        }

        //经过对两大类异或后,ret 存储的就是 a 和 b 两个数。
        return ret;        
    }
}

在这里插入图片描述

二:模拟

2.1 替换所有的问号

题目链接:替换所有的问号

在这里插入图片描述

在这里插入图片描述

class Solution {
    public String modifyString(String ss) {
        char[] s = ss.toCharArray();
        int n = s.length;
        
        for(int i = 0; i < n; i++){
            if(s[i] == '?'){
                for(char ch = 'a'; ch <= 'z'; ch++){
                    //同时判断左边是否相等和右边是否相等的情况
                    //接着再处理一下两个特殊情况,当 ? 为开头时只需判断右边
                    //当以 ? 为结尾时,只判断左边
                    if((i == 0 || ch != s[i - 1]) && (i == n - 1 || ch != s[i + 1])){
                        s[i] = ch;
                        break;
                    }
                }
            }
        }

        return String.valueOf(s);
        
    }
}

在这里插入图片描述

2.2 提莫攻击

题目链接:提莫攻击

在这里插入图片描述
在这里插入图片描述

class Solution {
    public int findPoisonedDuration(int[] timeSeries, int duration) {
        int ret = 0;
        for(int i = 1; i < timeSeries.length; i++){
            int x = timeSeries[i] - timeSeries[i - 1];
            if(x >= duration) ret += duration;
            else ret += x;
        }
        //不要忘了最后一次攻击还有持续时间
        return ret += duration;
        
    }
}

在这里插入图片描述

2.3 N 字形变换

题目链接:N 字形变换

在这里插入图片描述
在这里插入图片描述

class Solution {
    public String convert(String s, int numRows) {
        //处理一下边界情况
        if(numRows == 1) return s;

        //先定义第一行和最后一行相邻元素的距离差值 d
        int d = 2 * numRows - 2, n = s.length();
        //因为要频繁对字符串进行操作,但是 String 是不变的,因此此处使用 StringBuilder
        StringBuilder ret = new StringBuilder();

        //先处理第一行
        for(int i = 0; i < n; i += d){
            ret.append(s.charAt(i));
        }
        //接着处理中间行
        for(int k = 1; k < numRows - 1; k++){
            //因为到最后可能不满足这个规律了,所以 i < n || j < n 之间要用或
            for(int i = k, j = d - i; i < n || j < n; i += d, j += d ){
                //要防止越界
                if(i < n )ret.append(s.charAt(i));
                if(j < n ) ret.append(s.charAt(j));
            }
        }
        //处理最后一行
        for(int i = numRows - 1; i < n; i += d){
            ret.append(s.charAt(i));
        }
        
        return ret.toString();
    }
}

在这里插入图片描述

2.4 外观数列

题目链接:外观数列

在这里插入图片描述
在这里插入图片描述

class Solution {
    public String countAndSay(int n) {
        //先初始化这个字符为 1
        String ret = "1";
        //接着开始进行 n 次循环,因为这个字符的初始值为 1,所以我们直接从第二次迭代开始循环
        for(int i = 1; i < n; i++){
            //因为需要频繁的对字符串进行操作,此处我们使用 StringBuilder,用 ret 临时存储结果
            StringBuilder tmp = new StringBuilder();
            int len = ret.length();
            for (int left = 0, right = 0; right < len; ) {               
                while(right < len && ret.charAt(right) == ret.charAt(left)) right++;
               
                //循环结束后,ret.charAt(left)存储的是字符,right - left 存储的是这个字符的个数
                tmp.append(Integer.toString(right - left));
                tmp.append(ret.charAt(left));

                left = right;
            }
            //一层的逻辑处理完后把 tmp 赋给 ret ,继续进行下一轮
            ret = tmp.toString();
        }

        return ret;        
    }
}

在这里插入图片描述

2.5 数青蛙

题目链接:数青蛙

在这里插入图片描述
在这里插入图片描述

class Solution {
    public int minNumberOfFrogs(String c) {
        char[] croakOfFrogs = c.toCharArray();
        String t = "croak";// 定义青蛙叫声的顺序 "croak"
        int n = t.length();//青蛙叫声的长度

        // 创建一个映射表 `index`,用于将每个字符 'c', 'r', 'o', 'a', 'k' 映射到其在 "croak" 中的顺序位置
        Map<Character, Integer> index = new HashMap<>(); // [字符, 该字符在 "croak" 中的索引]
        for(int i = 0; i < n; i++) 
            index.put(t.charAt(i), i); // 'c' -> 0, 'r' -> 1, ..., 'k' -> 4

         // 使用数组 `hash` 来模拟哈希表,用于统计每个字符的进度状态,接下来开始遍历 croakOfFrogs 数组
        int[] hash = new int[n];
        for(char ch : croakOfFrogs){
            //先判断当前循环的字符是不是 c 
            if(ch == 'c'){ 
                if(hash[n - 1] != 0) hash[n - 1]--;
                hash[0]++;
            }else{
                //处理其他字符,首先获取当前字符在 "croak" 中的位置,方便进行判断
                int i = index.get(ch);
                if(hash[i - 1] == 0) return -1;
                else{hash[i - 1]--; hash[i]++;}
            }
        }

        // 检查前面四个字符是否已经全部匹配完成,如果有未完成的则返回 -1
        for(int i = 0; i < n - 1; i++){
            if(hash[i] != 0) return -1;
        }

        //返回 k 的值,此时的 k 值就代表最少青蛙的数
         return hash[n - 1];        
    }
}

在这里插入图片描述

三:分治 - 快排

3.1 颜色划分

题目链接:颜色划分

在这里插入图片描述
在这里插入图片描述

class Solution {
    // 辅助函数,用于交换数组 `nums` 中索引 `i` 和 `j` 的两个元素
    public void swap(int[] nums, int i, int j) {
        int t = nums[i];
        nums[i] = nums[j];
        nums[j] = t;
    }

    public void sortColors(int[] nums) {
         //先初始化 left 和 right ,要防止数组最左边是 0 ,最右边是 1,i 用于遍历数组
        int left = -1, right = nums.length, i = 0;

        while(i < right){
            if(nums[i] == 0) swap(nums, ++left, i++);
            else if(nums[i] == 1) i++;
            else swap(nums, i, --right);
        }
    }
}

在这里插入图片描述

3.2 排序数组 - 快速排序

题目链接:排序数组

在这里插入图片描述
在这里插入图片描述

class Solution {
    public int[] sortArray(int[] nums) {
        // 调用快速排序函数对整个数组进行排序
        qsort(nums, 0, nums.length - 1);
        return nums; // 返回排序后的数组
    }

    public void qsort(int[] nums, int l, int r){
        //先处理数组为空和数组只有 1 个元素的情况
        if(l >= r) return;

        //接着先获得一个数组范围内的随机基准值,[new Random(r - l + 1) 会生成一个介于 0 到 r - l 之间的随机整数,接着加上偏移量
        int key = nums[new Random().nextInt(r - l + 1) + l];
        //为了防止数组最左边是最小数和最右边是最大数的情况, i 用于遍历元素
        int left = l - 1, right = r + 1, i = l; 
        while(i < right){
            if(nums[i] < key) swap(nums, ++left, i++);
            else if(nums[i] == key) i++;
            else swap(nums, --right, i);
        }

        // 经过三分法分区后,数组分成了三部分:
        // [l, left]      - 小于基准值 key 的区域
        // [left + 1, right - 1] - 等于基准值 key 的区域
        // [right, r]     - 大于基准值 key 的区域
        //接着继续递归处理数组的左右两个区域
        qsort(nums, l, left);
        qsort(nums, right,r);
     }

      //封装一个 swap 函数
     public void swap(int[] nums,int i, int j){
        int t = nums[i];
        nums[i] = nums[j];
        nums[j] = t;
     }
}

在这里插入图片描述

3.3 数组中第 k 个大元素

题目链接:数组中第 k 个大元素

在这里插入图片描述

请添加图片描述

class Solution {
    public int findKthLargest(int[] nums, int k) {
        // 调用快速选择算法
        return qsort(nums, 0, nums.length - 1, k);
    }

     public int qsort(int[] nums, int l, int r, int k){
        //先处理只有一个元素的特殊情况
        if(r == l) return nums[r];

        //接着先对数组进行升序排序,并把数组分为三个区间
        //left 初始化为数组的最左边再 - 1,rigth 初始化为数组的最右边 +1 ,i 用来遍历数组,从给定的 l 开始遍历
        int left = l - 1, right = r + 1, i = l;
        int key = nums[new Random().nextInt(r - l + 1) + l]; // 随机选取基准
        while(i < right){
            if(nums[i] < key) swap(nums, ++left, i++);
            else if(nums[i] == key) i++;
            else swap(nums, --right, i);
        }

        //数组排序好了接着来分情况讨论寻找第 k 大的元素
        int c = r - right + 1;
        int b = right - left - 1;
        if(c >= k) return qsort(nums, right, r, k);
        else if(b + c >= k) return key;
        else return qsort(nums, l, left, k - b - c);
     }

     public void swap(int[] nums, int i, int j){
        int temp = nums[i];
        nums[i] = nums[j];
        nums[j] = temp;
     }
}

在这里插入图片描述

3.4 库存管理 III

题目链接:库存管理 III

在这里插入图片描述
在这里插入图片描述

class Solution {
    public int[] inventoryManagement(int[] stock, int cnt) {
        // 调用快速选择算法在数组中找到最小的 k 个数
        qsort(stock, 0, stock.length - 1, cnt);
        
        // 创建结果数组,存储最小的 k 个数
        int[] ret = new int[cnt];
        for (int i = 0; i < cnt; i++) {
            ret[i] = stock[i]; // 最小的 k 个数在排序后的前 k 个位置
        }
        return ret; // 返回结果数组
    }

     public void qsort(int[] nums, int l, int r, int k){
        //首先处理一下数组为空和只有一个元素的情况
        if(l >= r) return; 

        //接着开始对数组排序,排序前先初始化一下 left right 和 i
        int left = l - 1, right = r + 1, i = l;
        int key = nums[new Random().nextInt(r - l + 1) + l]; // 随机选取基准
        while(i < right){
            if(nums[i] < key) swap(nums, ++left, i++);
            else if(nums[i] == key) i++;
            else swap(nums, --right, i);
        }

        //数组排序好了接下来分情况讨论如果获取前 k 个小的元素
        int a = left - l + 1;
        int b = right - left - 1;
        if(a > k)  qsort(nums, l, left, k);
        else if(a + b >= k) return;  //不用接着排序了,直接返回
        else qsort(nums, right, r, k - b - a);
     }

     public void swap(int[] nums, int i, int j){
        int tmp = nums[i];
        nums[i] = nums[j];
        nums[j] = tmp;
     }
}

在这里插入图片描述

四: 分治 - 归并

4.1 排序数组 - 归并排序

题目链接:排序数组

在这里插入图片描述
请添加图片描述

class Solution {
   // 定义临时数组,用于存储合并过程中的元素
    int[] tmp;

    // 主函数,排序数组
    public int[] sortArray(int[] nums) {
        // 初始化临时数组,长度与待排序数组相同
        tmp = new int[nums.length];

        // 调用归并排序算法对整个数组进行排序
        mergeSort(nums, 0, nums.length - 1);

        // 返回排序后的数组
        return nums;
    }

     // 归并排序算法函数,对区间 [left, right] 进行排序
    public void mergeSort(int[] nums, int left, int right){
        //首先处理当前数组元素为空和只要有一个元素的情况,这种情况下数组无需排序了,直接返回
        if(left >= right) return;

        //如果不为空和只含有一个元素的话就开始把数组分为两部分,让两部分的数组有序
        int mid = left + (right - left) / 2;
        mergeSort(nums, left, mid);
        mergeSort(nums, mid + 1, right);

        //当数组的两个部分都有序了,我们来对数组的两个部分进行合并,把合并结果临时存储在 tmp 中
        //我们先用 cur1 标记数组第一部分的首元素索引,用 cur2 标记第二部分元素的首元素索引,用 i 来标记 tmp 中已有元素的个数
        int cur1 = left, cur2 = mid + 1, i = 0;
        while(cur1 <= mid && cur2 <= right)
            tmp[i++] = nums[cur1] < nums[cur2] ? nums[cur1++] : nums[cur2++];//先把小的数存入临时数组里
        //因为数组的两个部分的元素数量可能不同,所以我们接下来要把剩下的有序元素添加到 tmp 中
        while(cur1 <= mid)
            tmp[i++] = nums[cur1++];
        while(cur2 <= right)
            tmp[i++] = nums[cur2++];

        //最后把临时数组中的元素拷贝回原数组的 `[left, right]` 区间,只复制合并的部分
        for(int j = 0; j < i; j++){
            nums[j + left] = tmp[j];
        }

    }
}

在这里插入图片描述

4.2 交易逆序对的总数

题目链接:交易逆序对的总数

在这里插入图片描述
在这里插入图片描述
请添加图片描述

class Solution {
    // 定义临时数组,用于存储合并过程中的元素
    int[] tmp;

    // 主函数,计算数组中的逆序对个数
    public int reversePairs(int[] nums) {
        int n = nums.length;
        // 初始化临时数组,长度与待处理数组相同
        tmp = new int[n];
        // 调用归并排序并计算逆序对个数
        return mergeSort(nums, 0, n - 1);
    }

    // 归并排序函数,对区间 [left, right] 进行排序,并计算逆序对个数
    public int mergeSort(int[] nums, int left, int right){
        //先处理当数组只有一个元素或者元素为空的情况
        if(left >= right) return 0;

        //如果不是只含有一个元素或者为空,那么就开始处理这个数组的左右区间
        int mid = left + (right - left) / 2, ret = 0;
        ret += mergeSort(nums, left, mid);
        ret += mergeSort(nums, mid + 1, right);

        //当数组的左右部分都处理好了后开始合并数组,用 ret 存储逆序对的总数,i 来记录临时数组 tmp 已有元素的数量,cur1 记录左半部分数组的首元素索引, cur2 来记录右半部分的首元素索引
        int cur1 = left, cur2 = mid + 1, i = 0;
        while(cur1 <= mid && cur2 <= right){
            if(nums[cur1] <= nums[cur2]) tmp[i++] = nums[cur1++];//遍历的同时把元素拷贝到数组中进行数组升序
            else{ret += mid - cur1 + 1; tmp[i++] = nums[cur2++];}
        }
        //接着处理剩下的元素
        while(cur1 <= mid) tmp[i++] = nums[cur1++];
        while(cur2 <= right) tmp[i++] = nums[cur2++]; 

        //最后把临时数组中的元素拷贝回原数组的 `[left, right]` 区间,只复制合并的部分
        for(int j = 0; j < i; j++)
            nums[j + left] = tmp[j];

        return ret;
    }

}

在这里插入图片描述

4.3 计算右侧小于当前元素的个数

题目链接:计算右侧小于当前元素的个数

在这里插入图片描述

请添加图片描述

class Solution {
    // 定义辅助数组和计数数组
    int[] ret;         // 存储每个元素右侧比它小的元素个数
    int[] index;       // 标记 `nums` 中每个元素的原始下标
    int[] tmpIndex;    // 用于临时存储合并过程中元素的原始下标
    int[] tmpNums;     // 用于临时存储合并过程中元素的值

    // 主函数,返回每个元素右侧比它小的元素个数
    public List<Integer> countSmaller(int[] nums) {
        int n = nums.length;
        ret = new int[n];      // 初始化结果数组
        index = new int[n];    // 初始化下标数组
        tmpIndex = new int[n]; // 初始化临时下标数组
        tmpNums = new int[n];  // 初始化临时数值数组

        // 初始化 `index` 数组,使其指向 `nums` 中的每个元素的原始位置
        for (int i = 0; i < n; i++) {
            index[i] = i;
        }

        // 使用归并排序对 `nums` 数组进行排序,同时统计逆序对数量
        mergeSort(nums, 0, n - 1);

        // 将结果数组 `ret` 转换为列表形式并返回
        List<Integer> l = new ArrayList<Integer>();
        for (int x : ret) {
            l.add(x);
        }
        return l;
    }

     // 归并排序函数,对区间 [left, right] 进行排序,并统计右侧小于当前元素的个数
    public void mergeSort(int[] nums, int left, int right){
        //先处理数组为空和元素个数为一的情况
        if(left >= right) return;

        //如果不为空就开始处理数组的两个部分
        int mid = left + (right - left) / 2;
        mergeSort(nums, left, mid);
        mergeSort(nums, mid + 1, right);

        //当左右数组处理完毕后开始合并数组,并让数组降序
        int cur1 = left, cur2 = mid + 1, i = 0;
        while(cur1 <= mid && cur2 <= right){
            if(nums[cur1] <= nums[cur2]){
                tmpNums[i] = nums[cur2];//将当前右半部分的元素和它的原始位置放入临时数组 
                tmpIndex[i++] = index[cur2++];
            }else{
                ret[index[cur1]] += right - cur2 + 1; //index[cur1] 是当前元素 nums[cur1] 在原始数组中的下标
                tmpNums[i] = nums[cur1];//将当前左半部分的元素和它的原始位置放入临时数组 
                tmpIndex[i++] = index[cur1++];
            }
        }
        //接着处理剩下的元素
        while(cur1 <= mid){
            tmpNums[i] = nums[cur1];
            tmpIndex[i++] = index[cur1++];
        }
        while(cur2 <= right){
            tmpNums[i] = nums[cur2];
            tmpIndex[i++] = index[cur2++];
        }

         //最后把临时数组中的元素拷贝回原数组的 `[left, right]` 区间,只复制合并的部分
        for(int j = 0; j < i; j++){
            nums[j + left] = tmpNums[j];
            index[j + left] = tmpIndex[j];
        }
    }
}

在这里插入图片描述

4.4 翻转对

题目链接:翻转对
在这里插入图片描述

请添加图片描述

class Solution {
   int[] tmp; // 用于归并排序过程中存储排序的中间结果

    public int reversePairs(int[] nums) {
        int n = nums.length;
        tmp = new int[n]; // 初始化临时数组
        return mergeSort(nums, 0, n - 1); // 调用归并排序的递归函数
    }

    // 归并排序函数,同时计算翻转对的个数
    public int mergeSort(int[] nums, int left, int right){
        //先处理数组元素为空和数组元素为一的情况
        if(left >= right) return 0;

        //如果数组不为空和元素不为一,处理数组的左右两个部分,把数组左右两个部分的值累加到 ret 中
        int mid = left + (right - left) / 2, ret = 0;
        ret += mergeSort(nums, left, mid);
        ret += mergeSort(nums, mid + 1, right);

        // ret 累加完左右区间的翻转对后开始计算一左一右区间的翻转对
        int cur1 = left, cur2 = mid + 1;
        // 以 cur2 为参照,盯着 cur2 ,找当前元素后面有多少个元素的两倍比我小,数组要升序
        while(cur1 <= mid){
            while(cur2 <= right && (long)nums[cur1] > 2L * nums[cur2])  cur2++;// long 是因为两个数相乘范围太大,int可能回溢出
            //如果不是的话就让 ret 累加上 cur2 到 right 之间的元素,因为这些元素都满足翻转对的条件
            ret += cur2 - (mid + 1);; // 累加从 cur2 到 right 的元素数量
            cur1++;
        }

        // 当翻转和累加完毕后要开始合并左右区间了
        cur1 = left;
        cur2 = mid + 1; 
        int i = left;
        while(cur1 <= mid && cur2 <= right) tmp[i++] = nums[cur1] <= nums[cur2] ? nums[cur1++] : nums[cur2++];
        // 接着处理剩下的元素
        while(cur1 <= mid) tmp[i++] = nums[cur1++];
        while(cur2 <= right) tmp[i++] = nums[cur2++];

        //最后把临时数组中的元素拷贝回原数组的 `[left, right]` 区间,只复制合并的部分
        for (int j = left; j <= right; j++) 
            nums[j] = tmp[j];

        return ret;
    }
}

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ice___Cpu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值