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/
给定两个整数 a
和 b
,求它们的除法的商 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;
}
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· AI 智能体引爆开源社区「GitHub 热点速览」
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具