01-基础算法

1. 基础算法

1.1 基础排序算法

排序分为插入排序(直接插入排序、希尔排序),选择排序(选择排序、堆排序),交换排序(冒泡排序、快速排序),归并排序(归并排序)。

1.1.1 选择排序

找i-n范围内的最小值所在的位置,放到第i位。

public static void selectionSort(int[] arr){
    // 第i小的位置应该放哪个
    for (int i = 0; i < arr.length; i++) {
        // 从i-len-1之间找最小的,放到i位置。
        int min = i;
        for (int j = i+1; j < arr.length; j++) {
            if(arr[min] > arr[j]) min = j;
        }
        // 交换arr[i]和arr[j]
        int  temp = arr[i];
        arr[i] = arr[min];
        arr[min] = temp;
    }
}

1.1.2 冒泡排序

不断从前往后两两交换,让大的在最后。

public static void bubbleSort(int[] arr){
    // 第i大的冒泡到第n-i-1位置
    // 需要遍历的次数为n
    for (int i = 0; i < arr.length; i++) {
        // 遍历前arr.length-i-1个数
        // 因为相邻比较,所以向前探测一位,在for中留下一个
        for (int j = 0; j < arr.length-i-1; j++) {
            if(arr[j] > arr[j+1]){
                int temp = arr[j];
                arr[j] = arr[j+1];
                arr[j+1] = temp;
            }
        }
    }
}

1.1.3 直接插入排序

保证前i个数有序的情况下,通过交换让第i+1个数找到自己的位置。

public static void insertSort(int[] arr){
    // 保证0-i上是有序的
    for(int i = 1; i < arr.length; i++){
        // 记录当前数据
        int temp = arr[i];
        int j = 0;
        for(j = i-1; j >= 0 ; j--){
            // 注意:这里不能写arr[i],因为i位置的值已经被覆盖了
            if(temp < arr[j]){
                arr[j+1] = arr[j];
            }else{
                break;
            }
        }
        // 正常应该是把j位置的值放到j+1保存了,所以j空出来了
        // 因为多了一个--,所以+1
        arr[j+1] = temp;
    }
}

1.1.4 快速排序

1. 题目

快速排序

2. 思路

荷兰国旗问题:以最后一个数为基准,左面比他小,右面比他大,中间的和他相等 ,返回基准数相等的序列的开始结束位置。

对于当前的数cur:

​ 如果和基准相等:cur++

​ 如果大于基准:当前数和右边界交换,右边界扩张(因为不能确定交换过来的数的大小,所以不能判断)

​ 如果小于基准:当前数和左边界交换,左边界扩张,cur++(因为左边界外面的数不一定是cur,也可能是和基准数相等的数,所以要和cur换,而不是左边界直接扩张)

最后:

​ 由于右边界最右侧的数是基准条件,所以要把他和右边界交换(右边界内的数交换到右边界最右,不会混乱)

返回相等的区域

快速排序:

​ 由于最坏情况下(数组有序)时间复杂度是O(n2),所以引入随机,随便选一个数,和最后一个交换,变成基准。

​ 递归遍历(左,相等区域左边界-1),(右,相等区域右边界+1)

3. 代码

荷兰国旗:

public static int[] netherlandsFLag(int[]arr,int left,int right){
    // 处理不合理情况
    if(Math.max(left,right) >= arr.length || Math.min(left,right) < 0){
        return new int[] {-1,-1};
    }
    if(left == right){
        return new int[]{left,right};
    }
    // 定义左边界,右边界->没开始的时候,整个范围都没被纳入边界内
    // 因为最后一个数保留了,所以从right开始,而不是right+1
    int less = left-1, more = right;
    // 定义当前的数的位置
    int cur = left;
    // 如果前一个比他大
    while(cur < more){
        
        if(arr[cur] < arr[right]){
            // 纳入左边界,看下一个值
            // 注意:考虑左边界的下一个不一定为cur位置的值
            // 因为在下一个位置为和right位置的值相等的情况下
            // cur会向外进行
            swap(arr,cur,less+1);
            less++;
            cur++;
        }else if(arr[cur] > arr[right]){
            // 右边界前的一个交换,过来
            swap(arr,cur,more-1);
            // 右边界前一个的位置大小不知道,但是换过来的一定能纳入右边界
            more--;
        }else{
            // 相等就可以++
            cur++;
        }
    }

    // 交换最左面的数和右边1
    swap(arr,more,right);
    // 为什么是more:more-1是相等的,
    return new int[]{less+1,more};
}

快速排序主入口:

public static void quickSort(int[] arr){
    if(arr == null || arr.length == 0){
        return;
    }
    process(arr,0,arr.length-1);
}

递归函数:

private static void process(int[] arr, int l, int r) {
    if(l >= r){
        return;
    }
    // 最后一位和其他任意一个交换
    swap(arr,l+(int) Math.random()*(r-l+1),r);
    int[] equalArea = netherlandsFLag(arr,l,r);
    process(arr,l,equalArea[0]-1);
    process(arr,equalArea[1]+1,r);
}

1.2 二分法

二分法本质:删掉一半的数据,并且保证没删掉关键解。

二分法的核心在于边界问题的讨论,而边界的核心就是左右是否需要±1以及循环是否需要等号

是否需要等号:有等号代表我们出了循环之后得到的值就只有一个(l==r),而没有等号出循环之后得到的值是两个(l != r)。需要在循环外进行判断或者返回某一个需要的值。看±1,如果都有就可以等,少一个都要不等?存疑

是否需要±1:判断的依据是,±1是否会让我们跳过某一个解,会就不能这样,不会就可以±1。

1.2.1 是否存在某数

​ 在一个有序数组中,找某个数是否存在

public static int isContains(int[] arr,int val){
    int l = 0;
    int r = arr.length-1;
    while (l <= r){
        int mid = (l+r)/2;
        if(arr[mid] > val){
            r = mid-1;
        }else if(arr[mid] < val){
            l = mid+1;
        }else{
            return mid;
        }
    }
    return -1;
}

1.2.2 大于等于某数的最左位置

1. 题目

在一个有序数组中,找>=某个数最左侧的位置

2. 思路

利用一个遍历pos存放符合>= 的数的位置,而后二分,如果有符合的,二分后应该更加符合。

对于真正的一个数组想应该舍弃哪个位置,比想为什么这样好很多。

3. 代码

public static int leftestPos(int[] arr , int val){
    int l = 0;
    int r = arr.length-1;
    int pos = -1;
    while (l <= r){
        int mid = (l+r)/2;
        if(arr[mid] >= val){
            pos = mid;
            r = mid-1;
        }else if(arr[mid] < val){
            l = mid+1;
        }
    }
    return pos;
}

1.2.3 小于等于某数的最右位置

在一个有序数组中,找<=某个数最右侧的位置

同上。

public static int rightestPos(int[] arr, int val){
    int len = arr.length;
    int left = 0,right = len - 1;
    int pos = -1;
    while(left <= right){
        int mid = (left+right)/2;
        if(arr[mid] <= val){
            pos = mid;
            left = mid + 1;
        }else{
            right = mid - 1;
        }
    }
    return pos;
}

1.2.4 寻找峰值

1. 题目

https://leetcode.cn/problems/find-peak-element

峰值元素是指其值严格大于左右相邻值的元素。

给你一个整数数组 nums,找到峰值元素并返回其索引。数组可能包含多个峰值,在这种情况下,返回 任何一个峰值 所在位置即可。

你可以假设 nums[-1] = nums[n] = -∞ 。

你必须实现时间复杂度为 O(log n) 的算法来解决此问题。

示例 1:
输入:nums = [1,2,3,1]
输出:2
解释:3 是峰值元素,你的函数应该返回其索引 2。

示例 2:
输入:nums = [1,2,1,3,5,6,4]
输出:1 或 5 
解释:你的函数可以返回索引 1,其峰值元素为 2;
     或者返回索引 5, 其峰值元素为 6。

2. 思路

如果nums[0]比nums[1]大的话,就说明0位置为所求;如果nums[n]比nums[n-1]大的话,n位置为所求。

否则,二者(0,n)均为上升趋势,中间必有一个点为峰值。

case 1 : mid位置如果nums[mid-1] < nums[mid] > nums[mid+1],为所求,返回。

case 2 : mid位置如果nums[mid-1] < nums[mid] < nums[mid+1],

​ 只看nums[mid] < nums[mid+1],右侧为上升趋势,可以从右侧二分。

case 3 : mid位置如果nums[mid-1] > nums[mid] > nums[mid+1],

​ 只看nums[mid-1] > nums[mid],左侧为上升趋势,可以从左侧二分。

3. 代码

class Solution {
    public int findPeakElement(int[] nums) {
        if(nums.length == 1) return 0;
        int n = nums.length-1;
        if(nums[0] > nums[1]) return 0;
        if(nums[n] > nums[n-1]) return n;

        int l = 0;
        int r = n;
        while(l < r){
            int mid = (l+r)/2;
            // System.out.println(l+" "+r);
            if(nums[mid-1] < nums[mid] &&  nums[mid] > nums[mid+1]){
                return mid;
            }else if(nums[mid] < nums[mid+1]){
                // 考虑到mid,mid+1是上升趋势,但是mid+1之后不一定上升还是下降
                // 所以选择mid,而不是mid+1
                // 因此不能在while中加等号
                l = mid;
            }else{
                r = mid;
            }
        }
        return -1; 
    }
}

1.3 前缀和数组

1. 用处:频繁的求数组某个区间的和的时候可以用到。

2. 定义:presum[i] 为arr数组从0到i的前缀和,presum的长度和arr等长。

3. 用法:arr[i...j]的前缀和为presum[j]-presum[i-1];如果i为0的话,直接返回presum[j]即可。

4. 代码:

生成前缀和:

private static int[] setPresum(int[] arr) {
    int[] ps = new int[arr.length];
    // presum[i]为前i项的和
    ps[0] = arr[0];
    for (int i = 1; i < arr.length; i++) {
        ps[i] = ps[i-1] + arr[i];
    }
//        Arrays.stream(ps).forEach(System.out::println);
    return ps;
}

调用前缀和用O(1)得到区间和:

private static int getPresum(int[] ps, int l, int r) {
    return l == 0? ps[r] : ps[r] - ps[l-1];
}

1.4 二进制枚举

二进制枚举通过位运算来实现。

三种运算符:与&,或|,异或^。

两种操作:左移<<和右移>>。右移还分为带符号右移和无符号右移(>>>),无符号右移使用较少不做解释。

​ A<<B: 把A转化成二进制并且向左移动B位(末尾添加B个0);

​ A>>B: 把A转化成二进制并且向右移动B位(末尾删除B个0)。

​ A<<<B: 把A转化成二进制并且向左移动B位(移动包括符号位);

1.4.1 最大单词长度

1. 题目

https://leetcode-cn.com/problems/maximum-product-of-word-lengths/

给定一个字符串数组 words,请计算当两个字符串 words[i]words[j] 不包含相同字符时,它们长度的乘积的最大值。假设字符串中只包含英语的小写字母。如果没有不包含相同字符的一对字符串,返回 0。

示例 1:

输入: words = ["abcw","baz","foo","bar","fxyz","abcdef"]
输出: 16 
解释: 这两个单词为 "abcw", "fxyz"。它们不包含相同字符,且长度的乘积最大。

示例 2:

输入: words = ["a","ab","abc","d","cd","bcd","abcd"]
输出: 4 
解释: 这两个单词为 "ab", "cd"

示例 3:

输入: words = ["a","aa","aaa","aaaa"]
输出: 0 
解释: 不存在这样的两个单词。

2. 思路

预处理求出所有的word的单词哈希数组。

两个for遍历所有两两可能性。

优化:

​ 哈希数组为0-25的数组,但是我们只用知道这个位置有没有,而不需要知道他是多少,如果有是1,没有是0,则可以看成26位的二进制。因此可以当成一个int类型的数看待。比如3的二进制就是11,代表字符串只有a和b两个,多少不需要知道。

​ 如此一来,如果s1和s2有相同的字符,就意味着有某一位上二者的mask都为1,也就是m1&m2 != 0 。

3. 代码

class Solution {
    int[] nums;
    int[] parents;
    public int maxProduct(String[] words) {
        // 但是mask生成了太多次,能不能预处理mask,一个数组保存所有生成的mask,之后进行排序

        int len = words.length;
        int maxLenMul = 0;

        int[] masks = getMasks(words);
        for(int i = 0; i < len; i++){
            for(int j = i+1; j < len; j++){
                int curLenMul = words[i].length() * words[j].length();
                if(curLenMul > maxLenMul && (masks[i] & masks[j]) == 0){
                    maxLenMul = maxLenMul > curLenMul ? maxLenMul : curLenMul;
                }
            }
        }
        return maxLenMul;
    }

    public int[] getMasks(String[] words){
        int len = words.length;
        int[] ans = new int[len];

        for(int i = 0; i < len; i++){
            int mask = 0;
            int c2 = 0;
            for(int j = 0; j < words[i].length(); j++){
                int offset = words[i].charAt(j) - 'a';
                // if((mask & (1<<offset)) == 0) mask += ((1<<offset));
                mask = mask | (1<<offset);
            }
            ans[i] = mask;
        }
        return ans;
    }

}

1.5 两数异号

比较最高位,如果一样同号(异或结果为0),如果不一样,异号(异或结果为1)

return (a^b)>>>31 == 1;

1.6 快速幂

​ 对于幂乘来说,比如5的25次方,25的二进制位数为:11001。从右往左看也就是(5的一次方 * 1) *( 5的平方 * 0) * (5的四次方 * 0) * (5的八次方 * 1) *( 5的16次方 * 1),我们只需要一直让5翻倍即可,如果当前位置是1就乘起来。

​ 对于更一般的情况下,a的b次方,b的二进制有若干的1在某个位置。从右往左看,一直让a翻倍即可,直到b为0。

​ 只要b不为0就继续,如果(b&1) == 1 就让res *= a。之后,b右移一位,a翻倍。

// 快速求 a^b
public long fastPower(long a,long b){
    long res = 1;
    while (b != 0)
    {
        // b二进制中的1,表示这次可以将当前a的迭代值作为一项乘到res中
        if ((b & 1) != 0)
            res = res * a ;
        // b每次都会右移一位,每次我们都是处理最末位的二进制值
        b >>= 1; //b右移了一位后,a也需要更新
        // 迭代a,a在这里其实是每次的迭代值,等于a^{2^i},其中i从0开始
        a = a * a ;
    }
    System.out.println(res);
    return res;
}

1.6.1 两数相除

1. 题目

https://leetcode-cn.com/problems/divide-two-integers/

给定两个整数 ab ,求它们的除法的商 a/b ,要求不得使用乘号 '*'、除号 '/' 以及求余符号 '%'

注意:

  • 整数除法的结果应当截去(truncate)其小数部分,例如:truncate(8.345) = 8 以及 truncate(-2.7335) = -2
  • 假设我们的环境只能存储 32 位有符号整数,其数值范围是 [−231, 231−1]。本题中,如果除法结果溢出,则返回 231 − 1

2. 思路

如果直接暴力,最坏情况是21e的遍历次数,不行。

方法1:

考虑用快速乘来表示商,比如 45 / 7 = 6 。而6的二进制为: 110,也就是 4+2。

回到等式中,等式可以化简为:45 = 6 * 7 = 4 * 7 + 2 * 7;

4代表什么 ? 从大往小算,第一个与7相乘比45小的数,也就是4 * 7 <= 45,减去7 * 4之后,45还剩下20。

2代表什么? 从大往小算,第二个与7相乘比20(也就是45-28剩下的数)小的数。

推广到a和b,a / b = x,如果求x,只需要找到b*2^k <= a的值就一定在答案中,可以把他累加起来,就是答案了。

而b的2的n次方倍就可以写成b << n,为了考虑乘法溢出,当b<<i的时候比max/2大就可以返回了。

方法2:

b比a大就减,减之后b就翻倍,如果小了再缩回去,直到完成或者b为0(当a>b的时候,可能会出现b为0,而后死循环)

3. 代码

方法1:

    public int divide(int a, int b) {
        int max = Integer.MAX_VALUE;
        int min = Integer.MIN_VALUE;
        int shift = 0; // 左移的位数
        // 考虑边界
        if(a == b) return 1;
        if(a == min && b == -1) return max;

        // 计算符号,让ab都为负,保留符号
        boolean sign = true;
        if(a > 0){
            a = -a;
            sign = !sign;
        }
        if(b > 0){
            b = -b;
            sign = !sign;
        }


        // 左移到b比a大,然后再右移一位,找到最大的那个二进制的倍数
        // b比a靠近坐标轴,并且b右移之后比min/2小,注意是min,而且可以要等号,刚好不越界
        while(a < (b<<shift) && (b << shift) >= (min >> 1)) shift ++; // 右移+1
        //两个判断条件,看符合哪个
        // if(a > (b<<shift)) shift--;  // 没有这条也可以,因为下面循环会判断
        // if((b << shift) > (max >> 1)) 
        int ans = 0;
        while(a <= b){
            while(a > (b<<shift)) shift--;

            a -= (b<<shift);
            ans += (1<<shift);
        }
        return sign ? ans : -ans;

    }

方法2:

class Solution {
    public int divide(int a, int b) {
        if(a == Integer.MIN_VALUE && b == -1)   return Integer.MAX_VALUE;
       if(b == 1)   return a;
        int flag =  ((a > 0 && b < 0) || (a < 0 && b > 0))? 0 : 1; // 1正0负
        int sum = 0;
        int sumIncNum = 1;
       
        a = a > 0? -a:a;
        b = b > 0? -b:b;
         // 控制b的倍数
        int bCtrl = b;
        while(a <= b && bCtrl != 0){
            if(a <= bCtrl){
                a = a-bCtrl;
                sum += sumIncNum;
                bCtrl += bCtrl;
                sumIncNum += sumIncNum;

            }else{
                bCtrl = (bCtrl >> 1);
                sumIncNum = (sumIncNum >> 1);
            }
        }

        if(flag == 0){
            sum = -sum;
        }
        return sum;

    }
}

posted @   犹豫且败北  阅读(22)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· AI 智能体引爆开源社区「GitHub 热点速览」
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具
点击右上角即可分享
微信分享提示