文章目录
基础算法思想
动态规划
基础知识
-
概念:
动态规划 = 将原问题划分成一个个子问题(这些子问题存在重叠的情况),然后求解子问题,组合子问题的解得到原问题的解。
包含了分治思想、空间换时间、最优解等多种基石算法; -
适用场景:
适合动态规划的问题具有重叠子问题和最优子结构两大特性;
重叠子问题:即各个子问题中包含重复的更小子问题;使用暴力枚举,求解相同的子问题会产生大量的重复计算;而动态规划会将子问题的解保存,后续迭代查表即可,保证每个独立子问题只被计算一次;
最优子结构:如果一个问题的最优解可以由其子问题的最优解组合构成,并且这些子问题可以独立求解,那么称此问题具有最优子结构。 -
重叠子问题
记忆递归 = 自顶至低;
动态规划 = 自低至顶;
斐波那契数列问题并不包含最优子结构问题,只需要计算每个子问题的解,避免重复计算即可,并不需要从子问题组合中选择最优组合。 -
最优子结构
一个问题的最优解可以由其子问题最优解组合构成,那么称此问题具有最优子结构;
蛋糕售价最高问题:
不同重量的蛋糕售价不一样,已知总的蛋糕重量,求蛋糕的最大收益?
状态:
f(x)表示x重量的蛋糕的最高售价,其中f(0)=0 f(1) = p(1) = 2
p(x) 表示x重量的蛋糕价格;
分析:重量为n的蛋糕总价可切分为n种组合,即0,1,2...n-1 蛋糕的最高售价 加上 n,n-1,n-2....1剩余蛋糕的售价,组合中取最大值。
转移方程:
f(n) = f(i) + p(n-i)的最大值,i属于0到n之间
表示i重量蛋糕的最高售价 + n-i重量的蛋糕价格。
解题步骤
-
1 确定状态
解动态规划需要创建一个数组,数组的每个元素f[i] 或者 f[i][j] 表示什么;
步骤:研究最优策略的最后一步;化为子问题; -
2 初始条件、边界情况
初始条件:f[0] f[1]
边界条件:数组的边界、越不越界问题 -
3 转移方程
根据子问题定义直接得到 -
4 计算顺序
利用之前的计算结果
基本步骤
- 分析最优解的性质,并刻画其结构特征;
- 递归地定义最优解;
- 以自底向上的方式或自顶向下的记忆化方法,计算最优解;
- 根据计算最优解时得到的信息,构造一个最优解。
分治与动态规划的区别
区别 | 动态规划 | 分治方法 |
---|---|---|
子问题划分 | 将问题划分成子问题有重叠的情况 不同的子问题具有公共的子子问题 | 将问题划分成互不相交的子问题 |
是否会反复求解公共子子问题 | 否 | 是 |
分治
基础知识
- 概念:
把一个复杂的问题分成多个相同或类似的子问题,再分成子子问题,直到最后子问题可以简单的直接求解。原问题的解 = 子问题解的合并。 - 设计思想:
把一个难以解决的大问题,分割成一个个规模较小的问题,以便个个击破,分而治之。 - 分治策略:
一个规模为n的问题,若可以容易解决,那直接解决即可;否则将其分解为k个规模较小的子问题。
子问题相互独立,且与原问题形式相同;
递归求解子问题,然后将子问题的解合并得到原问题的解;
适用场景
问题缩小到一定程度可以解决;
可分解为规模较小的相同问题,即该问题存在最优子结构;
分解出的子问题的解可以合并成原问题的解;
原问题分解出的子问题 是相互独立的,子问题之间不包含公共的子子问题;
解题步骤
分解、解决、合并。
回溯算法
基础知识
-
概念:
类似枚举的搜索尝试过程。在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就“回溯”返回,尝试别的路径。
满足回溯条件的某个状态点称为“回溯点”。 -
搜索过程:
搜索解空间树,按照深度优先搜索的策略,从根节点出发深度搜索解空间树;
当搜索到某一节点时,要先判断该节点是否包含问题的解;
如果包含,就从该节点出发继续探索下去;如果不包含,则逐层向其祖先元素节点回溯。
解题步骤
定义一个解空间,包含所有的解;
利用适于搜索的方法组织解空间;
利用深度优先法搜索解空间;
利用限界函数避免移动到不可能产生解的子空间。
广度优先搜索(BFS Breadth-First Search)
基本过程:从根节点开始,沿着树的宽度遍历树的节点。如果所有节点均被访问,则算法中止。
通常借助队列的先进先出特性来实现。
具体操作:
把起始节点放入queue
重复下述两步,直至queue为空为止;
从queue中取出队列头的节点;
找出与此点临近的且未遍历的点,进行标记,然后全部放入queue中。
做题步骤
根判断是否为空,为空返回;
队列根元素先进;
循环取队头;
看值左右子节点,如果存在就进队列;
适用场景
解决
为了解决不定次数的循环问题。
固定模板
void backTrack(原始输入集,层数,临时结果,结果){
if(临时结果满足输入要求(一般是集合长度满足)){
临时结果加入到结果集;
return;
}
控制层数不要超过题目要求长度{
当前元素加入到临时结果集;
backTrack(原始输入集,层数+1,临时结果,结果);
当前元素移出临时结果集;
}
}
贪心
基础知识
概念:
将求解过程分成若干个步骤,且每个步骤都是使用贪心原则,选取当前状态下最好/最优的选择,并以此希望最后堆叠出的结果也是最优的。
解题步骤
①从某个初始解出发;
②通过迭代的方式,当可以向目标前进一步时,就根据局部优化策略,得到一部分解,缩小问题规模;
③将所有解综合起来。
学习案例:找零钱问题
需求:钱币是25分、10分、5分、1分。需要总和是41分、且硬币数量要最少。
思路分析:
为了满足硬币数最少的原则。
1 能找25分就不找别的。那么此时可以分1个25分。
41 - 25 = 16
2 能找10分就不找别的 10分1个
16 - 10 = 6
3 能找5分就不找别的 5分1个
6 - 5 = 1
4 最后剩余的全部是1分
1 /1 = 1
总数 4个硬币。
不会写代码。2022/8/9
递归
基础知识
概念:
以编程的角度来看,递归指的是方法定义中调用方法本身的现象。
解决问题的思路:
①把一个复杂的问题层层转化为一个与原问题相似的规模较小的问题来解决;
②递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算过程;
递归解决问题要找出两个内容:
①递归出口:否则会出现内存溢出;
②递归规则:与原问题相似的规模较小的问题;
备注2022/8/11
- Java方法的递归:
一个方法的内部调用自身的过程;
递归必须要有结束条件;
递归次数不能太多,太多会导致内存溢出;
解题步骤
做题分析
考虑项 | 实现 |
---|---|
递归出口 | 分析递归结束的条件 |
递归规则 | 分析递归工作顺利进行的条件 |
回溯阶段 | 递归具体执行的操作 |
迭代和递归
区别 | 递归 recursion | 迭代 iteration |
---|---|---|
概念 | 运行的过程中调用自己,调用自身的编程思想,即一个函数调用本身 重复调用函数自身实现循环 | 又称为辗转法,利用已知的变量值,根据递推公式不断演进得到变量新值的编程思想 函数内某段代码实现循环 与之相对的直接法(一次解法),即一次性解决问题 |
产生条件 | 子问题须与原始问题为同样的事,且更为简单 不能无限制地调用本身,须有个出口,化简为非递归状态处理 | – |
案例 | 斐波那契数列、阶乘、汉诺塔问题、全排列 | 斐波那契数列、汉诺塔问题、背包问题 |
结构 | 树结构,从递归到达底部就开始回归,过程相当于树的深度优先遍历 | 环结构,从初始状态开始,每次迭代都遍历这个环,并更新状态,多次迭代直到结束状态 |
递归
第一要素:明确函数的功能;
第二要素:寻找递归结束的条件;
第三要素:找出函数的等价关系;因为要不断缩小参数范围,要通过辅助变量或操作,使得原函数的结果不变;
递归、迭代与普通循环的区别
递归与普通循环的区别:
递归—有去有回(因为存在终止条件);循环—有去无回;
迭代与普通循环的区别:
迭代—循环代码中参与运算的变量同时是保存结果的变量,当前保存的结果作为下一次循环计算的初始值;循环—当前保存的结果不一定作为下一次循环计算的开始;
递归与迭代之间转换
理论上,两者可以相互转换,但实际从算法结构来说,迭代可以转换为递归、但递归不一定能转换为迭代;
递归转为非递归算法(两种方式)
区别 | 直接转换法 | 间接转换法 |
---|---|---|
需求 | 直接求值(迭代),不需要回溯 使用一些变量保存中间结果 | 不能直接求值,需要回溯 使用栈保存中间结果 |
概念 | 通常用来消除尾递归(tail recursion)和单向递归,将递归结构用迭代结构来替代 单向递归 → 尾递归 → 迭代 | 递归实际利用系统堆栈实现自身调用,该方法使用栈保存中间结果模拟递归过程,转为非递归形式 尾递归函数调用返回时正好是函数的结尾,因此递归调用时就不需要保留当前栈帧,可以直接将当前栈帧覆盖掉 |
比较排序算法
算法时间复杂度和空间复杂度
比较排序 | 平均时间复杂度 | 最坏情况 | 最好情况 | 空间复杂度 | 稳定性 |
---|---|---|---|---|---|
交换排序 | |||||
冒泡排序 | O(n2) | O(n2) | O(n) | O(1) | 稳定 |
快速排序 | O(n log2n) | O(n2) | O(n log2n) | O(nlog2n) | 不稳定 |
选择排序 | |||||
简单选择排序 | O(n2) | O(n2) | O(n2) | O(1) | 不稳定 |
堆排序 | O(n log2n) | O(n log2n) | O(nlog2n) | O(1) | 不稳定 |
插入排序 | |||||
直接插入排序 | O(n2) | O(n2) | O(n) | O(1) | 稳定 |
希尔排序 | O(n1.3) | O(n2 ) | O(n) | O(1) | 不稳定 |
归并排序 | |||||
归并排序 | O(n log2n) | O(n log2n) | O(nlog2n) | O(n) | 稳定 |
非比较排序 | 平均时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 | 稳定性 |
---|---|---|---|---|---|
计数排序 | O(n+k) | O(n+k) | O(n+k) | O(n+k) | 稳定 |
基数排序 | O(n*k) | O(n*k) | O(n*k) | O(n+k) | 稳定 |
桶排序 | O(n+k) | O(n2) | O(n) | O(n+k) | 稳定 |
稳定性
稳定 = 原本a在b之前,且a=b,排序之后a仍然在b之前;
不稳定 = 原本a在b之前,且a = b,排序之后a可能会出现在b之后;
交换排序
冒泡排序
原理:
从第一个元素开始不断比较相邻的两个元素,如果第一个大于第二个元素,则交换位置,直到整个数列为有序数列;
最终结果是:最大的数在最后一位,第二大的放在倒数第二位;
代码思想:
1 两层循环,外层循环用于控制每趟比较的次数;内层循环描述具体的比较;
2 外层循环用于控制内层循环比较的的次数;内层循环每执行一次,外层循环控制变量就需要减1;
代码
public static int[] bubbleSort(int[] arr){
// 定义数组的长度
int len = arr.length;
// 交换元素的暂存元素
int temp;
// 用于控制循环末端结束的位置;
// 比如 第一次循环 结束位置是len-2 第二次 结束位置是len-3
while(len > 0){
// 具体的比较代码
for(int i = 0 ; i < len-1; i ++){
if(arr[i] > arr[i+1]){
temp = arr[i+1];
arr[i+1] = arr[i];
arr[i] = temp;
}
}
len--;
}
return arr;
}
稳定性分析
冒泡排序本质是前后两个元素比较大小,前面大于后面,前后元素调换;
而对于相等的元素来说,前后位置不会发生变化,则稳定;
快速排序+递归
原理:
从数列中取出一个数,将比这个值大的放在它的右边,将比这个值小的放在它的左边,左右两边再重复这个过程,直到各个区域只有一个数。
代码思想
1 采用递归的形式实现,也就是方法自己调用自己;
2 方法所需参数是 数组 左下标left 右下标right;递归结束条件是left >= right;
3 选left表示元素为目标判断值,先找right下标元素小于target的值;再找left下标元素大于target的值;两者交换
循环继续的条件是left < right;
4 当left == right的时候,将目标值赋值给left下标所处位置;
5 递归的规则:
方法(arr,left,lo-1); // 左侧数组
方法(arr,lo+1,right); // 右侧数组
代码
public static void quickRom(int[] arr,int left,int right){
// 递归结束条件,也就是排序的数组中只有一个数据
if(left >= right)
return;
// 定义三个变量,lo = 比目标小的值的索引,hi = 比目标大的值的索引
int lo = left;
int hi = rihgt;
// 定义中间数据是左侧的数据
int target = arr[left];
// 保证左侧节点的下标小于右侧的;
while(lo < hi){
// 从右向左找 比节点小的数 循环结束 要么找到 要么lo = hi
while(arr[hi] >= target && lo < hi)
hi --;
// 从左往右找 比节点大的数 循环结束 要么找到 要么lo = hi
while(arr[lo] <= target && lo < hi)
lo ++;
// 找到 左侧大于中间值的数 右侧小于中间值的数 交换
if(lo < hi){
int temp = arr[lo];
arr[lo] = arr[hi];
arr[hi] = temp;
}
}
// lo == hi 一轮循环结束 此位置就是target所在的目标位置
arr[left] = arr[lo]; //将lo位置元素给了left位置,
arr[lo] = target;
// 对lo == hi 位置 左右数组再进行重复操作
quickRom(arr,left,lo-1);
quickRom(arr,lo+1,right);
}
稳定性
不稳定的;
1 首先找target元素,将小于它的元素放在它的左侧,大于它的元素放在它的右侧;
2 对于相同大小的元素,他们的相对位置会发生变化;
假设5 3 3 4 3 8 9 10 11
目标元素是5 则最后的样子是 3 3 3 4 5 8 9 10 11
这样就将原始第三个3 放在了第一个3之前。
选择排序
将数组中的最大(最小)数依次先出来。
简单的选择排序
原理
找出数组中的最小值,然后与第一位交换,然后再找出第二小的值与第二位交换。
最后实现数组从小到大的排列。
设计思路
1 双层循环,外层表示最小/最大的数;内层用于遍历剩余元素;
2 内层中的if,判断内层元素与外层固定元素的比值;
代码
public static void simpleRom(int[] arr){
for(int i = 0 ; i < arr.length-1; i++){
// 定义最小值 以及最小值的索引
int minIndex = i;
int min = arr[i];
for(int j = i +1; j < arr.length ; j++){
if(min > arr[j]){
min = arr[j];
minIndex = j;
}
}
// 用于判断min最小值是否更新过 更新过需要进行修改,将最小值所处下标的位置 存储arr[i]的元素;
if(minIndex != i){
arr[minIndex] = arr[i];
arr[i] = min;
}
}
}
稳定性
xhj:主要判断是相等元素之间的相对位置是否发生变化
会发生变化,比如{5,8,9,5,6,2}
数组编程升序数组,则索引0放最小值,外层循环一次时 数组变成了{2,8,5,9,6,5}
交换之后,原始的第一个5在第二个5之后了。
与冒泡排序的区别
区别 | 冒泡 | 简单选择排序 |
---|---|---|
比较不同 | 比较相邻位置的两个数 | 按顺序比较,找最大值或最小值 |
交换次数 | 每轮比较后,位置不对都需要换位置 | 每一轮比较只需要换一次位置 |
本质 | 通过数去找位置 | 通过给定位置去找数 |
堆排序
概述
堆排序 是一棵顺序存储的完全二叉树;
每个节点的关键字都不大于其孩子节点的关键字 = 小根堆;升序初始创建大顶堆;
每个节点的关键字都不小于其孩子节点的关键字 = 大根堆;降序初始创建小顶堆;
a[i] >= a[2i+1] && a[i] >= a[2i+2]; //大根堆
a[i] <= a[2i+1] && a[i] <= a[2i+2]; //小根堆
整体由创建初始堆 + 交换堆顶元素和末尾元素并重建堆 两部分组成;
前者时间复杂度是O(n);后者时间复杂度是O(nlogn)。
完全二叉树扩展
叶子结点只能出现在最下层和次下层;
完全二叉树的叶子节点数:
n为奇数(度为1的节点数为0),n0 = (n+1)/2; 非叶子节点数正好相反;
n为偶数(度为1的节点数为1),n0 = n / 2;非叶子节点数 正好相反;
代码原理
①将无序序列构建成一个堆,根据升序降序需求选择 小根堆 或 大根堆;
遍历:
从最后一个非叶子节点开始,依次将索引减小;假设要创建大根堆
i = 数组长度/2 -1 ; i >= 0 i-- 这是当数组长度是奇数 偶数的时候是(数组长度+1)/2 -1
遍历里面的条件判断:
找i节点的子结点,就是k = 2*i+1 左子节点;判断两个子结点那个更大;
找到大的那个 然后和i节点比较,如果i节点小,则两者交换;否则 break退出 for循环;
②将堆顶元素与末尾元素交换,将最大元素“沉”到数组末端;
创建方法swap(int[] arr,int a,int b)交换堆顶和堆低的元素
③重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换,直到整个序列有序。
递归调用创建堆的过程。
代码
实现:构建大顶堆;交换堆顶堆低元素+ 递归调整堆;==》结果是小顶堆
public static void sort(int[] arr){
// 1构建大顶堆
for(int i = arr.length /2-1; i >= 0 ; i--){
// 从第一个非叶子节点 从下至上,从右至左的调整结构。
// arr.length /2 表示节点数是奇数时的非叶子节点数,和叶子节点数 表示正好相反;
adjustHeap(arr,i,arr.length);
}
// 2 调整堆结构 + 交换堆顶元素与堆低元素
for(int j = arr.lenght-1 ; j >0; j--){
// 从堆低元素与末尾元素进行交换
swap(arr,0,j);
// 重新对堆进行调整
adjustHeap(arr,0,j);
}
}
// 建立大顶堆
public static void adjustHeap(int[] arr,int i,int length){
int temp = arr[i]; // 取根元素
for(int k = i*2 + 1 ; k < length ; k = k*2+1){ //从i节点的左子节点开始,即2i+1;
if(k+1 < length && arr[k] < arr[k+1]) // k 指向i节点的子节点中 较大的一个;
k++;
if(arr[k] > temp){
arr[i] = arr[k];
i = k ; // 更新父节点位置
}else{
break;
}
}
arr[i] = temp;
}
// 交换元素
public void swap(int[] arr,int a,int b){
int temp = arr[a];
arr[a] = arr[b];
arr[b] = temp;
}
代码
实现:构建小顶堆;调整堆结构+交换堆顶堆低元素;==》 最终结果是大顶堆
public static void sort(int[] arr){
// 构建初始堆
// 从最后一个非叶子节点开始 向上遍历 arr.length == 奇数 则arr.length/2 ;反之为(arr.length+1)/2
for(int i = arr.length/2 ; i >=0 ; i--){
类对象.method(arr,0,arr.length);
}
// 堆顶、堆低元素交换 以及 重构堆结构
for(int j = len-1; j >= 0; j--){
swap(arr,0,j);
method(arr,0,j);
}
}
public void method(int[] arr,int i,int len){
// 找i下标所在位置的子结点
int temp = arr[i];
for(int k = 2*i+1 ; k < len ; k = k*2+1){
if(k+1 < len && arr[k] > arr[k+1])
k++;
if(arr[i] > arr[k]){
arr[i] = arr[k];
i = k;
}else{
break;
}
}
arr[i] = temp;
}
// 两个元素交换
public void swap(int[] arr,int a,int b){
int temp = arr[a];
arr[a] = arr[b];
arr[b] = temp;
}
堆排序不稳定说明
插入排序
直接插入排序
原理
对于未排序数据,在已排序序列中从后向前扫描(可以顺序查找、折半查找),找到相应的位置插入即可。
代码
public static void simpleInsert(int[] arr){
for(int i = 0 ; i < arr.length ; i++){
// 保存要插入的值
int temp = arr[i];
// 判断要插入的位置在哪里
int j;
// 依次与该元素前面的元素比较 比i元素大的后移
for(j = i-1; j>= 0 ; j--){
if(arr[j] > temp)
arr[j+1] = arr[j];
else
break;
}
arr[j+1] = temp;
}
}
希尔排序
出现原因
为了解决简单插入排序插入数字较小,后移次数变量造成的效率低的问题;
原理
与简单插入排序不同,简单插入排序依次比较相邻的数,而希尔排序优先比较距离较远的元素。
又称为缩小增量排序,通过分组进行比较。
如果直接间隙为1,则就是简单插入排序;间隙大于1的所有操作都是为了间隙为1的操作打基础。
思想
首先区别于简单插入算法,前者依次比较相邻的数;后者比较距离较远的元素;
通过gap = arr.length/2 进行数组划分;
分析
初始: 9 1 2 5 7 4 8 6 3 5
gap 的值 5 2 1 需要间隔gap的值进行比较。
代码
public static void shellSort(int[] arr){
// gap 表示间隔几个元素进行比较。
for(int gap = arr.length/2 ; gap > 0; gap /= 2){
// i 表示从第i个元素开始进行排序。
for(int i = gap; i <arr.length;i++){
// j表示具体要操作的下标值;temp存储i下标的元素
int j = i;
int temp = arr[i];
// 从小到大排列,如果前者元素值大于后者 则需要进行交换 判断
if(arr[j] < arr[j-gap]){
// 循环判断
while(j-gap >= 0 && temp < arr[j-gap]){
arr[j] = arr[j-gap];
j -= gap;
}
arr[j] = temp;
}
}
}
}
归并排序
原理
该排序算法采用分治策略(将问题分成一些小的问题然后递归求解);
指将两个顺序序列合并成一个顺序序列的方法;
然后合并相邻有序子序列;
图示
代码
import java.util.*;
public class Xhj{
public static void main(String[] args){
int[] arr = {8,5,3,6,9,7,-1};
int[] temp = new int[arr.length];
mergeSort(arr,0,arr.length-1,temp);
System.out.println(Arrays.toString(temp));
}
public static void mergeSort(int[] arr,int left,int right,int[] temp){
if(left < right){
int mid = (left + right) / 2;
//向左递归分解
mergeSort(arr,left,mid,temp);
//向右递归分界
mergeSort(arr,mid+1,right,temp);
//合并
merge(arr,left,mid,right,temp);
}
}
public static void merge(int[] arr,int left,int mid,int right,int[] temp){
int i = left; //左边有序序列的初始索引
int j = mid +1; // 右边有序序列的初始索引
int t = 0 ; // 合并数组的起始索引
// 比较两个指针指向元素的大小,选择相对较小的元素放入合并区间,并移动指针到下一个位置
while(i <= mid && j <= right){
if(arr[i] < arr[j]){
temp[t] = arr[i];
t += 1;
i += 1;
} else{
temp[t] = arr[j];
t += 1;
j += 1;
}
}
// 循环实现 直到有一侧数组的索引溢出
// 将有序数组的其余元素合并到temp数组中
while(i <= mid){
temp[t] = arr[i];
t += 1;
i += 1;
}
while(j <= right){
temp[t] = arr[j];
t += 1;
j += 1;
}
// 最后将中间数组的结果复制到原始数组中
t = 0;
int newLeft = left;
while(newLeft <= right){
arr[newLeft] = temp[t];
newLeft += 1;
t += 1;
}
}
}
非比较排序
计数排序
原理
创建一个数组C,长度取决于待排序数组中数据的范围,将排序数值对应的C数组+1。
也就是 排序数组中数据的范围是0-n则数组C的长度 = n+1
也就是说 创建的数组C,用于存放待排序数组对应数据出现的个数。
算法思想
1 遍历数组找到数组中的最大值与最小值;
2 创建新数组,用于存储数组中每个元素出现的个数;
3 按照区间数组中的次数一次将元素打印出来即可。
代码2022/8/24书写
import java.util.*;
public class Xhj{
public void sort(int[] arr){
// 遍历数组元素的最大值和最小值
int min,max;
for(int an:arr){
if(an > max)
max = an;
if(an < min)
min = an;
}
// 创建计数数组 下标j用于遍历原始数组
int[] arrCount = new int[max-min+1];
int j = 0;
// i用于遍历计数数组
for(int i =0; i < arrCount.length ; i++ ){
if(arrCount[i] != 0){
arr[j] = min + i;
j++;
}
}
}
}
基数排序
概念
按照数字的位数从低位到高位依次排序,先按照个位排序、再按照十位排序、再按照百位排序;
理解:
一串数字:12,12,56,577,93
先按照个位排序:12,12,93,56,577
再按照十位排序:12,12,56,577,93
再按照百位排序:12,12,56,93,577
代码思想
判断给定数字中最大数的位数:
每次排序只有位数不一样,排序的代码基本相同,使用循环实现;
打印排序好的数。
import java.util.*;
public class Main{
public static void main(String[] args){
int[] arr = {326,453,608,835,751,435,704,690,88,79,79};
System.out.println("基数排序前为:");
for(int t : arr){
System.out.print(t + " ");
}
System.out.println();
System.out.println("---------------------------------");
int len = getRadix(arr);
radixSorting(arr,len);
}
// 获取数组中最大值的长度
public static int getRadix(int[] arr){
int max = arr[0];
for(int i = 0 ; i < arr.length; i ++){
if(arr[i] > max)
max = arr[i];
}
return (max + "").length();
}
// 进行基数排序
public static void radixSorting(int[] arr,int len){
for(int i = 0 ; i < len ; i++){
arr = countingSort(arr,i);
print(arr,i+1,len);
}
}
// 基数排序每次排序的输出
public static void print(int[] arr,int k,int d){
if(k==d){
System.out.println("最终排序结果是:");
}else{
System.out.println("按第" + k + "位排序后,结果为:");
}
System.out.println(Arrays.toString(arr));
}
// 利用计数排序对元素的每一位进行排序
public static int[] countingSort(int[] arr,int index){
int k = 9;
int[] b = new int[arr.length];
int[] c = new int[k+1]; // 存储数组每个元素对应位数 的数量。
// 初始化 k+1个桶 0-8的桶的值都是0 没有给9号赋初值
for(int i = 0 ; i < k; i++){
c[i] = 0;
}
// 统计数组元素第index位的值 并计算和写入c数组中
for(int i = 0; i < arr.length ; i++){
int d = getBitData(arr[i],index);
c[d]++;
}
for(int i = 1 ; i <= k; i++){
c[i] += c[i-1];
}
for (int i = arr.length - 1; i >= 0; i--) {
int d = getBitData(arr[i], index);
b[c[d] - 1] = arr[i];//C[d]-1 就代表小于等于元素d的元素个数,就是d在B的位置
c[d]--;
}
return b;
}
// 获取data指定位index的值
public static int getBitData(int data,int index){
while(data != 0 && index > 0){
data /= 10;
index --;
}
return data % 10;
}
}
结果
桶排序
查找算法
顺序(线性)查找
原理
循环遍历数组去查找所要的元素,找到就返回下标,没有就返回-1,适用于有序无序;
代码
import java.util.*;
class Main{
public static void main(String[] args){
String[] arr = new String[]{"AA","BB","DD","tt","yy"};
String target = "tt";
int result = method(arr,target);
System.out.println(result);
}
public static int method(String[] arr,String target){
for(int i = 0 ; i < arr.length ; i++){
if(arr[i].equals(target))
return i;
}
return -1;
}
}
二分查找/折半查找
原理
使用前提是:线性表中的记录必须是关键码有序(通常是从小到大)、表必须采用顺序存储;
思路
把中间值作为比较对象,如果target值与中间记录关键字相等则查找成功;
如果target值小于中间值,则说明在左侧区间中;
如果target值大于中间值,则说明在右侧区间中;
代码
使用的是递归,如果使用while循环结果会很慢。
import java.util.*;
class Main{
public static void main(String[] args){
int[] arr = {1,5,6,8,9,10};
int result = method1(arr,0,5,5);
System.out.println(result);
}
public static int method1(int[] arr,int left,int right,int target){
if(left > right)
return -1;
int mid = (left + right) / 2;
if(arr[mid] > target){
return method1(arr,left,mid-1,target);
}else if(arr[mid] < target){
return method1(arr,mid+1,right,target);
}else{
return mid;
}
}
}
字符数组、整型、字符串
所属类 | 方法 | 参数类型 | 说明 |
---|---|---|---|
数组.length | – | 求数组的长度 | |
Arrays | static void sort(参数) | 参数可以是 byte[] a、char[] a、double[] a、 float[] a、int[] a、short[] a、 Object[] a | 对数组进行升序排列 |
Arrays | static String toString(数组) | 基本数据类型/Object类型数组 | 返回指定数组的内容的字符串形式 |
Collection | boolean add(E e) | 基本类型包装类、对象 | 添加元素 |
Collection | int size() | – | 返回集合的长度,也是集合中元素的个数 |
Collection | Iterator < E > iterator() | – | 返回此集合中元素的迭代器 |
Collection | boolean remove(Object o) | 对象、基本数据包装类 | 从集合中移除指定的元素 |
Map | V put(K key,V value) | 对象、基本类型包装类 | 添加元素,返回值是value |
Map | V remove(Object key) | 对象、基本类型包装类 | 根据键删除键值对对象 |
Map | Boolean containKey(Object key) | 对象、基本类型包装类 | 判断集合是否包含指定的键 |
Map | boolean containsValue(Object value) | 对象、基本类型包装类 | 判断集合是否包含指定的值 |
Map | boolean isEmpty() | – | 判断集合是否为空 |
Map | V get(Object key) | 对象、基本类型包装类 | 根据键获取值 |
Map | Set < K > keySet() | – | 获取所有键的集合 |
Map | Collection < V > values() | – | 获取所有值的集合 |
Map | Set < Map.Entry < K,V > > entrySet() | – | 获取所有键值对 对象的集合 |
List | void add(int index,E element) | int类型;对象、基本数据类型包装类 | 在集合中的指定位置插入元素 |
List | E remove(int index) | int类型 | 删除集合中指定索引处的元素,并返回该元素 |
List | E get(int index) | int类型 | 获取集合中指定索引处的元素,并返回该元素 |
List | E set(int index,E element) | int类型、对象或基本类型包装类 | 给集合中指定索引位置重新赋值,并返回该元素 |
Collections | static void swap(List< T > list,int i,int j) | int类型 | 交换指定列表中指定位置的元素 |