常见算法和Lambda
常见算法
查找算法
基本查找
从0索引开始逐个查找
代码演示:
package Search;
import java.util.ArrayList;
public class BasicSearch {
public static void main(String[] args) {
int[] arr = {1,2,3,4,5,6,3,4,1,3};
ArrayList<Integer> resArr = basicFind(arr, 3);
for (int i = 0; i < resArr.size(); i++) {
System.out.print(resArr.get(i) + " ");
}
}
public static ArrayList<Integer> basicFind(int[] arr, int num){
ArrayList<Integer> res = new ArrayList<>();
int index = 0;
for (int i = 0; i < arr.length; i++) {
if(arr[i] == num) res.add(i);
}
return res;
}
}
二分查找
概述
前提条件: 数组中的数据必须有序
核心逻辑: 每次排除一半的查找范围
public static int binarySearch(int[] arr, int num){
int n = arr.length;
int l = 0, r = n - 1;
while(l <= r){
int mid = l + (r - l) / 2;
if(arr[mid] == num) return mid;
else if(arr[mid] > num){
r = mid - 1;
}
else{
l = mid + 1;
}
}
return -1;
}
总结
1.二分查找的优势?
- 提前查找效率
2.二分查找的前提条件?
- 数据必须是有序的
- 如果数据是乱的,先排席再用二分查找得到的索引没有实际意义,只能确定当前数字在数组中是否存在,因为排序之后数字的位置就可能发生变化了
3.二分查找的过程
- min和max表示当前要查找的范围
- mid是在min和max中间的
- 如果要查找的元素在mid的左边,缩小范围时min不变,max等于mid减1如果要查找的元素在mid的右边,缩小范围时, max不变,min等于mid加1
插值查找
计算mid的时候可以进行改进
m
i
d
=
m
i
n
+
k
e
y
−
a
r
r
[
m
i
n
]
a
r
r
[
m
a
x
]
−
a
r
r
[
m
i
n
]
∗
(
m
a
x
−
m
i
n
)
mid = min + \frac{key - arr[min]}{arr[max] - arr[min]} * (max - min)
mid=min+arr[max]−arr[min]key−arr[min]∗(max−min)
也叫插值查找, 数组中的数据分布比较均匀的话会更快
分块查找
分块的原则1: 块内无序, 块之间有序
分块原则2 : 块数数量一般等于总数开根号
核心思路: 先确定要查找的元素在哪一块, 然后在块内逐个查找
实现步骤
- 创建数组blockArr存放每一个块对象的信息
- 先查找blockArr确定要查找的数据属于哪一块
- 在单独遍历这一块数据即可
代码:
package Search;
public class BlockSearch {
public static void main(String[] args) {
int[] arr = {16, 5, 9, 12, 21, 18,
32, 23, 37, 26, 45, 34,
50, 48, 61, 52, 73, 66};
// 创建数组
Block b1 = new Block(21, 0, 5);
Block b2 = new Block(45, 6, 11);
Block b3 = new Block(73, 12, 17);
Block[] block = {b1, b2, b3};
int index = blockSearch(arr, block, 37);
System.out.println(index);
}
public static int blockSearch(int[] arr, Block[] block, int num){
// 先判断num属于哪一块
int blockIndex = getBlockIndex(block, num);
int startIndex = block[blockIndex].getStartIndex();
int endIndex = block[blockIndex].getEndIndex();
for(int i = startIndex; i <= endIndex; ++i){
if(arr[i] == num){
return i;
}
}
return -1;
}
// 找出num属于哪个分块
private static int getBlockIndex(Block[] block, int num) {
for (int i = 0; i < block.length; i++) {
if(num <= block[i].getBlockMax()){
return i;
}
}
return -1;
}
}
// 标准JavaBean
class Block{
private int blockMax;
private int startIndex;
private int endIndex;
public int getBlockMax() {
return blockMax;
}
public Block() {
}
public Block(int blockMax, int startIndex, int endIndex) {
this.blockMax = blockMax;
this.startIndex = startIndex;
this.endIndex = endIndex;
}
public void setBlockMax(int blockMax) {
this.blockMax = blockMax;
}
public int getStartIndex() {
return startIndex;
}
public void setStartIndex(int startIndex) {
this.startIndex = startIndex;
}
public int getEndIndex() {
return endIndex;
}
public void setEndIndex(int endIndex) {
this.endIndex = endIndex;
}
}
扩展的分块查找(无规律的数据)
排序算法
冒泡排序
相邻的数据两两比较, 小的放前面, 大的放后面
第一轮循环结束, 最大值已经找到, 在数组的最右边
第二轮循环只要在剩余的元素找最大值就可以了 …
代码演示:
package SortDemo;
public class BubbleSort {
public static void main(String[] args) {
int[] arr = {2,4,5,6,3,1,7,12,9,8,10,11,0};
myBubbleSort(arr);
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + " ");
}
}
// 外循环表示轮数, 有n个数据, 就要执行n-1轮
private static void myBubbleSort(int[] arr) {
int n = arr.length;
for (int i = 0; i < n; ++i) {
boolean swapped = false;
// 内循环, 每一轮中找到最大值
// -1是为了防止数组越界
// -i为了提高效率, 每一轮执行的次数比上一轮少执行一次
for (int j = 0; j < n - i - 1; ++j){
if(arr[j] > arr[j+1]){
int temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
swapped = true;
}
}
// 如果遍历一轮都没发生交换,说明数组已经有序,直接返回即可
if(!swapped) return;
}
}
}
选择排序
从索引0开始, 拿着每一个索引上的元素跟后面的元素依次比较, 小的放前面, 大的放后面, 以此类推
第一轮循环结束后, 最小的数据已经确定 …
private static void mySelectSort(int[] arr) {
int n = arr.length;
for(int i = 0; i < n; ++i){
int min = i;
for(int j = i + 1; j < n; ++j){
if(arr[j] < arr[min]) min = j;
}
if(min != i) swap(arr, min, i);
}
}
插入排序
插入排序:
将0索引的元素到N索引的元素看做是有序的,把N+1索引的元素到最后一个当成是无序的。
遍历无序的数据,将遍历到的元素插入有序序列中适当的位置,如遇到相同数据,插在后面。
N的范围:0~最大索引
public class InsertSort {
public static void main(String[] args) {
int[] arr = {3,44,38,5,47,15,36,26,27,2,46,4,19,50,48};
myInsertSort(arr);
myPrint(arr);
}
private static void myPrint(int[] arr) {
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + " ");
}
System.out.println();
}
private static void myInsertSort(int[] arr) {
if(arr.length <= 1) return;
for(int i = 1; i < arr.length; ++i){
// 记录当前要插入的数据索引
int j = i;
while(j > 0 && arr[j] < arr[j-1]){
// 交换位置
int temp = arr[j];
arr[j] = arr[j-1];
arr[j-1] = temp;
--j;
}
}
}
}
快速排序
递归算法
概述
递归是指方法中调用方法本身的现象
程序书写不当会导致栈溢出, 递归一定要有出口
作用:
- 把一个复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解
- 递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算
递归的两个核心
- 找出口:什么时候不再调用方法。
- 找规则 如何把大问题变成规模较小的问题
递归练习
求1-100的和
public class QuickSort {
public static void main(String[] args) {
// 求1-100之间的和
System.out.println(getSum(100));
}
public static int getSum(int num){
// 终止条件 出口
if(num == 1){
return 1;
}
return num + getSum(num - 1); // 规律
}
}
求5的阶乘
public static int getJieC(int num){
if(num == 2) return 2;
return num * getJieC(num - 1);
}
方法内部再次调用方法的时候, 参数应该比上次更靠近出口
内存图
快速排序
第一轮: 把0索引的数字作为基准数,确定基准数在数组中正确的位置
比基准数小的全部在左边,比基准数大的全部在右边。
import java.util.Random;
public class QuickSort {
public static void main(String[] args) {
// int[] arr = {6,1,2,7,9,3,4,5,10,8};
int[] arr = new int[1000000];
Random r = new Random();
for(int i = 0; i < arr.length; ++i){
arr[i] = r.nextInt();
}
long time1 = System.currentTimeMillis();
myQuickSort(arr, 0, arr.length - 1);
long time2 = System.currentTimeMillis();
//myPrint(arr);
System.out.println(time2 - time1); // 141
}
private static void myPrint(int[] arr) {
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + " ");
}
System.out.println();
}
private static void myQuickSort(int[] arr, int i, int j) {
int start = i;
int end = j;
if(start >= end) return;
while(start != end){
// 用end找到第一个比arr[i]小的数据
while(start < end && arr[end] >= arr[i]) --end;
// 用start找到第一个比arr[i]大的数据
while(start < end && arr[start] <= arr[i]) ++start;
// 交换start和end的元素
int temp = arr[start];
arr[start] = arr[end];
arr[end] = temp;
}
// 此时start和end指向同一元素
// 即arr[i]应该处于的位置
int temp = arr[i];
arr[i] = arr[start];
arr[start] = temp;
// 递归排序左边和右边
myQuickSort(arr, i, start - 1);
myQuickSort(arr, start + 1, j);
}
能不能先找start后找end?
不行, 最后基准数归位的时候, 会把一个大于arr[i] 的移动到前面
总结:
1.冒泡排序:
相邻的元素两两比较,小的放前面,大的放后面
2.选择排序:
从0索引开始,拿着每一个索引上的元素跟后面的元素依次比较
小的放前面,大的放后面,以此类推
3.插入排序:
将数组分为有序和无序两组,遍历无序数据,将元素插入有序序列中即可,
- 4.快速排序
将排序范围中的第一个数字作为基准数,再定义两个变量start,endstart - 从前往后找比基准数大的,end从后往前找比基准数小的。
- 找到之后交换start和end指向的元素,并循环这一过程,直到start和end处于同一个位置,该位置是基准数在数组中应存入的位置,再让基准数归位。
- 归位后的效果:基准数左边的,比基准数小,基准数右边的,比基准数大
Arrays
常用方法
没有构造方法
方法基本都是静态的
操作数组的工具类:
package ArrayTest;
import java.util.ArrayList;
import java.util.Arrays;
public class ArrayDemo1 {
public static void main(String[] args) {
test01();
}
private static void test01() {
int[] arr = {1,2,3,4,5};
System.out.println(Arrays.toString(arr)); // [1, 2, 3, 4, 5]
// toString()方法的底层就是用StringBuilder实现拼接的
int index1 = Arrays.binarySearch(arr, 5); // 二分查找元素
System.out.println(index1); // 4
int index2 = Arrays.binarySearch(arr, 10);
System.out.println(index2); // -6
// 细节1: 元素需要为升序的
// 细节2: 元素存在, 返回对应下标
// 元素不存在的话, 返回其应该插入位置的索引的负数-1
// 10 应该插入在索引5处, 返回-5-1
// -1 是因为插入在索引0的时候, -0会给人误导
int[] arr2 = Arrays.copyOf(arr, 5);
System.out.println(Arrays.toString(arr2)); // [1, 2, 3, 4, 5]
// 细节: 第二个参数小于第一个数组的长度是, 会从头截取
// 等于的话 完全拷贝
// 大于的话会用初始值填充
int[] arr3 = Arrays.copyOf(arr, 3);
System.out.println(Arrays.toString(arr3)); // [1, 2, 3]
int[] arr4 = Arrays.copyOf(arr, 10);
System.out.println(Arrays.toString(arr4)); // [1, 2, 3, 4, 5, 0, 0, 0, 0, 0]
int[] arr5 = Arrays.copyOfRange(arr, 1, 5);
// 包头不包尾 包左不包右
System.out.println(Arrays.toString(arr5)); // [2, 3, 4, 5]
Arrays.fill(arr, 10);
System.out.println(Arrays.toString(arr)); // [10, 10, 10, 10, 10]
int[] arr6 = new int[1];
Arrays.fill(arr6, 100);
System.out.println(Arrays.toString(arr6)); // [100]
// 就是说会根据初始数组的长度进行填充
// 如果数组长度为0, 打印出来就是[]
int[] arr7 = {3,6,1,9,10,4,8,2,5,7};
Arrays.sort(arr7);
System.out.println(Arrays.toString(arr7)); // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
// 这个方法默认使用快速排序
}
}
sort方法的重载
package ArrayTest;
import java.util.Arrays;
import java.util.Comparator;
public class SortDemo {
public static void main(String[] args) {
// sort参数 要排序的数组 排序规则
// 细节: 只能给引用数据类型的数组进行排序
// 如果数组是基本数据类型的, 需要变成其对应的包装类
Integer[] arr = {3,6,1,9,10,4,8,2,5,7};
//第二个参数是一个接口,所以我们在调用方法的时候,需要传递这个接口的实现类对象,作为排序的规则。
//但是这个实现类,我只要使用一次,所以就没有必要单独的去写一个类,直接采取匿名内部类的方式就可以了
//底层原理:
//利用插入排序 + 二分查找的方式进行排序的。
// 默认把0索引的数据当做是有序的序列,1索引到最后认为是无序的序列。
// 遍历无序的序列得到里面的每一个元素,假设当前遍历得到的元素是A元素
// 把A往有序序列中进行插入,在插入的时候,是利用三分查找确定A元素的插入点。
// 拿着A元素,跟插入点的元素进行比较,比较的规则就是compare方法的方法体
// 如果方法的返回值是负数,拿着A继续跟前面的数据进行比较
// 如果方法的返回值是正数,拿着A继续跟后面的数揭进行比较
// 如果方法的返回值是e,也拿着A跟后面的数据进行比较
// 直到能确定A的最终位置为止。
// compare方法的形式参数:
// 参数1: o1 表示在无序序列中, 遍历得到的每一个元素
// 参数2: o2 表示有序序列的元素
//返回值
//负数: 表示当前要插入的元素是小的,放在前面
//正数: 表示当前要插入的元素是大的,放在后面
//0: 表示当前要插入的元素跟现在的元素比是一样的们也会放在后面
Arrays.sort(arr, new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
// return o1 - o2;// 升序排序
return o2 - o1; // 降序排序
}
});
System.out.println(Arrays.toString(arr));
}
}
Lambda表达式
用lambda表达式对sort进行简化
函数式编程
是一种思想特点, 忽略面向对象的复杂语法, 强调做什么, 而不是谁去做
面向对象: 先找对象, 让对象做事情
Lambda表达式的标准格式
package LambdaTest;
import java.util.Arrays;
public class LambdaDemo {
public static void main(String[] args) {
sortByLambda();
}
private static void sortByLambda() {
Integer[] arr = {9,3,6,1,4,10,7,2,8,5};
Arrays.sort(arr, (Integer a, Integer b) -> {
return b - a;
});
System.out.println(Arrays.toString(arr));
}
}
注意点:
- Lambda表达式可以用来简化匿名内部类的书写
- Lambda表达式只能简化函数式接口的匿名内部类的写法
- 函数式接口:
有且仅有一个抽象方法的接口叫做函数式接口,接口上方可以加@Functionallnterface注解
package LambdaTest;
public class LambdaDemo2 {
public static void main(String[] args) {
// 利用匿名内部类
// 调用一个方法的时候, 如果方法的形参是一个接口, 那么要传递这个接口的实现类对象
// 如果实现类对象只要用到一次, 就可以用匿名内部类的形式进行书写
// method(new Swim() {
// @Override
// public void swim() {
// System.out.println("swiming");
// }
// });
// lambda表达式
method(()->{
System.out.println("swiming");
});
}
private static void method(Swim s) {
s.swim();
}
}
@FunctionalInterface
interface Swim{
abstract void swim();
}
小结:
lambda基本作用: 简化函数式接口匿名内部类的写法
使用前提: 必须是接口的匿名内部类, 接口中只能有一个抽象方法
好处: 简化代码
Lambda表达式的省略写法
省略核心: 可推导, 可省略
省略规则:
- 参数类型可以省略不写
- 如果只有一个参数, 参数类型可以省略, 同时() 也可以省略
- 如果Lambda表达式的方法体只有一行, 大括号, 分号, return可以省略不写, 需要同时省略
package LambdaTest;
import java.util.Arrays;
public class LambdaDemo {
public static void main(String[] args) {
sortByLambda();
}
private static void sortByLambda() {
Integer[] arr = {9,3,6,1,4,10,7,2,8,5};
Arrays.sort(arr, (a, b) -> b - a);
System.out.println(Arrays.toString(arr));
}
}
练习:
定义数组并存储一些字符串,利用Arrays中的sort方法进行排序
要求:
按照字符串的长度进行排序,长的在前面,短的在后面(暂时不比较字符串里面的内容)
package LambdaTest;
import java.util.Arrays;
public class LambdaDemo3 {
public static void main(String[] args) {
String[] arr = {"asd", "q", "fdsfe", "vfrfsa", "vc", "hjgy"};
Arrays.sort(arr, (String s1, String s2) -> s2.length() - s1.length());
System.out.println(Arrays.toString(arr)); // [vfrfsa, fdsfe, hjgy, asd, vc, q]
}
}
综合练习
练习1
定义数组并存储一些女朋友对象,利用Arrays中的sort方法进行排序
要求1:属性有姓名、年龄、身高。
要求2:按照年龄的大小进行排序,年龄一样,按照身高排序,身高一样按照姓名的字母进行排序.(姓名中不要有中文或特殊字符,会涉及到后面的知识)
JavaBean类
package Practice;
public class GirlFriend {
private String name;
private int age;
private int height;
public GirlFriend() {
}
public GirlFriend(String name, int age, int height) {
this.name = name;
this.age = age;
this.height = height;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public int getHeight() {
return height;
}
public void setHeight(int height) {
this.height = height;
}
@Override
public String toString() {
return "GirlFriend{" +
"name='" + name + '\'' +
", age=" + age +
", height=" + height +
'}';
}
}
测试类:
package Practice;
import java.util.Arrays;
public class Test01 {
public static void main(String[] args) {
GirlFriend g1 = new GirlFriend("qwe", 20, 168);
GirlFriend g2 = new GirlFriend("efhgtg", 18, 173);
GirlFriend g3 = new GirlFriend("tyghh", 22, 170);
GirlFriend g4 = new GirlFriend("rtgfd", 18, 169);
GirlFriend g5 = new GirlFriend("vfdgt", 20, 168);
// System.out.println(g1);
GirlFriend[] arr = {g1, g2, g3, g4, g5};
// for (int i = 0; i < arr.length; i++) {
// System.out.println(arr[i]);
// }
// 按照年龄的大小进行排序,年龄一样,按照身高排序,
// 身高一样按照姓名的字母进行排序
// 年龄升序 升高降序 名字字典升序
Arrays.sort(arr, (g01, g02) -> {
if(g01.getAge() == g02.getAge() && g01.getHeight() == g02.getHeight()){
return g01.getName().compareTo(g02.getName());
} else if (g01.getAge() == g02.getAge()) {
return g02.getHeight() - g01.getHeight();
}else {
return g01.getAge() - g02.getAge();
}
});
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}
}
}
练习2
有一个很有名的数学逻辑题叫做不死神兔问题,小兔子长到第三个月后每个月又生一对兔子,假如兔子都不死,问第十二个月的兔子对数为多少?
package Practice;
public class Test02 {
public static void main(String[] args) {
int month = 12;
int res = getSum(month);
System.out.println(res);
}
private static int getSum(int month) {
if(month == 1 || month == 2) return 1;
return getSum(month - 1) + getSum(month - 2);
}
}
练习3
有一堆桃子,猴子第一天吃了其中的一半,并多吃了一个!以后每天猴子都吃当前剩下来的一半,然后再多吃一个,第10天的时候(还没吃),发现只剩下一个桃子了,请问,最初总共多少个桃子?
package Practice;
public class Test03 {
public static void main(String[] args) {
int num = 10;
int res = getSum(num);
System.out.println(res);
}
private static int getSum(int num) {
if(num == 1) return 1;
return (getSum(num-1) + 1) * 2;
}
}
练习4
可爱的小明特别喜欢爬楼梯,他有的时候一次爬一个台阶,有的时候一次爬两个台阶。
如果这个楼梯有20个台阶,小明一共有多少种爬法呢?
运算结果:
1层台阶1种爬法
2层台阶2种爬法
7层台阶 21种爬法
package Practice;
public class Test04 {
public static void main(String[] args) {
int num = 20;
int res = getSum(num);
System.out.println(res);
}
private static int getSum(int num) {
if(num == 1) return 1;
if(num == 2) return 2;
return getSum(num - 1) + getSum(num - 2);
}
}
扩展: 还可以爬三层(动态规划做法)
package Practice;
public class Test05 {
public static void main(String[] args) {
int num = 20;
int res = getSum(num);
System.out.println(res);
}
private static int getSum(int num) {
if (num == 1) return 1;
if (num == 2) return 2;
if (num == 3) return 4;
int[] dp = new int[num + 1];
dp[1] = 1;
dp[2] = 2;
dp[3] = 4;
for (int i = 4; i <= num; i++) {
dp[i] = dp[i - 1] + dp[i - 2] + dp[i - 3];
}
return dp[num];
}
}