查找算法
基本查找(顺序查找)
顺序查找也称为线形查找,属于无序查找算法。从数据结构线的一端开始,顺序扫描,依次将遍历到的结点与要查找的值相比较,若相等则表示查找成功;若遍历结束仍没有找到相同的,表示查找失败。
public static void main(String[] args) {
//基本查找/顺序查找
//核心:
//从0索引开始挨个往后查找
//需求:定义一个方法利用基本查找,查询某个元素是否存在
//数据如下:{131, 127, 147, 81, 103, 23, 7, 79}
int[] arr = {131, 127, 147, 81, 103, 23, 7, 79};
int number = 82;
System.out.println(basicSearch(arr, number));
}
//参数:
//一:数组
//二:要查找的元素
//返回值:
//元素是否存在
public static boolean basicSearch(int[] arr, int number){
//利用基本查找来查找number在数组中是否存在
for (int i = 0; i < arr.length; i++) {
if(arr[i] == number){
return true;
}
}
return false;
}
二分查找
也叫做折半查找,元素必须是有序的,从小到大,或者从大到小都是可以的。如果是无序的,也可以先进行排序。但是排序之后,会改变原有数据的顺序,查找出来元素位置跟原来的元素可能是不一样的,所以排序之后再查找只能判断当前数据是否在容器当中,返回的索引无实际的意义。
核心逻辑:一次排除一半
public static void main(String[] args) {
//二分查找/折半查找
//核心:
//每次排除一半的查找范围
//需求:定义一个方法利用二分查找,查询某个元素在数组中的索引
//数据如下:{7, 23, 79, 81, 103, 127, 131, 147}
int[] arr = {7, 23, 79, 81, 103, 127, 131, 147};
System.out.println(binarySearch(arr, 150));
}
public static int binarySearch(int[] arr, int number){
//1.定义两个变量记录要查找的范围
int min = 0;
int max = arr.length - 1;
//2.利用循环不断的去找要查找的数据
while(true){
if(min > max){
return -1;
}
//3.找到min和max的中间位置
int mid = (min + max) / 2;
//4.拿着mid指向的元素跟要查找的元素进行比较
if(arr[mid] > number){
//4.1 number在mid的左边
//min不变,max = mid - 1;
max = mid - 1;
}else if(arr[mid] < number){
//4.2 number在mid的右边
//max不变,min = mid + 1;
min = mid + 1;
}else{
//4.3 number跟mid指向的元素一样
//找到了
return mid;
}
}
}
插值查找
如果我能在二分查找的基础上,让中间的mid点,尽可能靠近想要查找的元素,就更能提高查找的效率了。二分查找中原来查找点计算如下:mid=(low+high)/2, 即mid=low+1/2*(high-low);我们可以将查找的点改进为如下:mid=low+(key-a[low])/(a[high]-a[low])*(high-low),这样,让mid值的变化更靠近关键字key,这样也就间接地减少了比较次数。可以理解成根据要查找的值在数组数据值中的比例,缩短查找路径。但是有局限,数据分布均匀的值用这种效果会好一点,不均匀反而会变差;只要把中点计算方式修改一下就好。
斐波那契查找
这个就比较复杂了,先粗略估算数组长度,再在斐波那契数列找一个值,然后将数组扩展为这个数目的元素,(本质上也是一种二分查找)并对这些元素进行分割,缩小范围,进行查找;这个理论上要更高效,这就是数学上的东西了,我数学不好,就不卖弄了,这个不作为重点。
分块查找
当数据表中的数据元素很多时,可以采用分块查找。汲取了顺序查找和折半查找各自的优点,既有动态结构,又适于快速查找。分块查找适用于数据较多,但是数据不会发生变化的情况,如果需要一边添加一边查找,建议使用哈希查找。
分块查找过程:
- 需要把数据分成N多小块,块与块之间不能有数据重复的交集。
- 给每一块创建对象单独存储到数组当中
- 查找数据的时候,先在数组查,当前数据属于哪一块
- 再到这一块中顺序查找
分块原则:
- 前一块最大值要小于后一块所有;
- 块数量为数字个数开平方根;
扩展原则:
- 数据没有明显规律时候分块按照不存在交集的逻辑去分
- 分完之后确定每一块的最大最小值
- 接着对这些块采用二分法或者顺序遍历看看要查找的数据在不在范围内
- 在的话遍历找到索引即可
哈希查找后面再说;
排序算法
冒泡排序
核心逻辑:相邻数据两两比较,小的放前面,大的放后面(升序);
public static void main(String[] args) {
/*
冒泡排序:
核心思想:
1,相邻的元素两两比较,大的放右边,小的放左边。
2,第一轮比较完毕之后,最大值就已经确定,第二轮可以少循环一次,后面以此类推。
3,如果数组中有n个数据,总共我们只要执行n-1轮的代码就可以。
*/
//1.定义数组
int[] arr = {2, 4, 5, 3, 1};
//2.利用冒泡排序将数组中的数据变成 1 2 3 4 5
//外循环:表示我要执行多少轮。 如果有n个数据,那么执行n - 1 轮
for (int i = 0; i < arr.length - 1; i++) {
//内循环:每一轮中我如何比较数据并找到当前的最大值
//-1:为了防止索引越界
//-i:提高效率,每一轮执行的次数应该比上一轮少一次。
for (int j = 0; j < arr.length - 1 - i; j++) {
//i 依次表示数组中的每一个索引:0 1 2 3 4
if(arr[j] > arr[j + 1]){
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
printArr(arr);
}
private static void printArr(int[] arr) {
//3.遍历数组
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + " ");
}
System.out.println();
}
选择排序
非常像之前比较大小时候采用的“擂台赛”,其过程(升序)如下:
- 从0索引开始,就假定它是最小的,然后依次跟后面的数据比大小,如果后面的数据比他小,那么就交换这两个位置的数据,继续拿着0索引的值向后比完,这样一轮下来,第一个位置必定是最小值;
- 从索引1开始重复上述过程,重复数组长度-1次就好;
public static void main(String[] args) {
/*
选择排序:
1,从0索引开始,跟后面的元素一一比较。
2,小的放前面,大的放后面。
3,第一次循环结束后,最小的数据已经确定。
4,第二次循环从1索引开始以此类推。
*/
//1.定义数组
int[] arr = {2, 4, 5, 3, 1};
//2.利用选择排序让数组变成 1 2 3 4 5
//外循环:几轮
//i:表示这一轮中,我拿着哪个索引上的数据跟后面的数据进行比较并交换
for (int i = 0; i < arr.length -1; i++) {
//内循环:每一轮我要干什么事情?
//拿着i跟i后面的数据进行比较交换
for (int j = i + 1; j < arr.length; j++) {
if(arr[i] > arr[j]){
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
}
printArr(arr);
}
private static void printArr(int[] arr) {
//3.遍历数组
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + " ");
}
System.out.println();
}
插入排序
核心是从左向右(升序),先遍历一次数组,看看左边到哪个位置开始不是升序了,这时,就按照这个位置将数组划分为两部分,左边有序的一部分和右边无序的一部分。将无序部分开始的索引记下来,从它开始遍历无序的一部分一次插入左边有序的那一部分;插入时候,从有序的那一部分由左向右或者由右向左利用二分法或者依次遍历插入到合适位置,当无序那一部分没有值了,遍历结束,排序结束。
public static void main(String[] args) {
/*
插入排序:
将0索引的元素到N索引的元素看做是有序的,把N+1索引的元素到最后一个当成是无序的。
遍历无序的数据,将遍历到的元素插入有序序列中适当的位置,如遇到相同数据,插在后面。
N的范围:0~最大索引
*/
int[] arr = {3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48};
//1.找到无序的哪一组数组是从哪个索引开始的。 2
int startIndex = -1;
for (int i = 0; i < arr.length; i++) {
if(arr[i] > arr[i + 1]){
startIndex = i + 1;
break;
}
}
//2.遍历从startIndex开始到最后一个元素,依次得到无序的哪一组数据中的每一个元素
for (int i = startIndex; 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--;
}
}
printArr(arr);
}
private static void printArr(int[] arr) {
//3.遍历数组
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + " ");
}
System.out.println();
}
快速排序
先了解递归:递归是指方法自己调用自己的过程;
直接递归自己会导致无限套娃下去,从而内存溢出,所以一定要有出口;
举例:用递归求1~100的和:
public static void main(String[] args) {
System.out.println(getSum(100));
}
public static int getSum(int num) {
if (num == 1) {
return 1;
}
return num + getSum(num - 1);
}
快速排序(升序)借用的就是递归思想:
- 第一次排序时候把0索引记录的值拿出来,然后把1索引的位置定义为start,数组最后一位定义为end;
- 然后先拿end位置的值和0索引的值比,如果发现0索引的值大于end索引的值,end指针停住,接着去执行start索引,拿它来和0索引的值比,如果0索引的值小于start索引的值,就停住start指针;
- 这时,将start索引的值和end索引的值交换,交换完成后继续2步骤,直到end和start指向了同一个数据,这时候拿0索引的基准值和这个值交换位置,即找到了0索引对应数字的正确位置。
- 这时候,0索引的位置归位,把交换出来的值递归调用该方法,找到全部的正确位置后,排序完成;
/*
快速排序:
第一轮:以0索引的数字为基准数,确定基准数在数组中正确的位置。
比基准数小的全部在左边,比基准数大的全部在右边。
后面以此类推。
*/
int[] arr = {1,1, 6, 2, 7, 9, 3, 4, 5, 1,10, 8};
//int[] arr = new int[1000000];
/* Random r = new Random();
for (int i = 0; i < arr.length; i++) {
arr[i] = r.nextInt();
}*/
long start = System.currentTimeMillis();
quickSort(arr, 0, arr.length - 1);
long end = System.currentTimeMillis();
System.out.println(end - start);//149
System.out.println(Arrays.toString(arr));
//课堂练习:
//可以利用相同的办法去测试一下,选择排序,冒泡排序以及插入排序运行的效率
//得到一个结论:快速排序真的非常快。
/* for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + " ");
}*/
}
/*
* 参数一:我们要排序的数组
* 参数二:要排序数组的起始索引
* 参数三:要排序数组的结束索引
* */
public static void quickSort(int[] arr, int i, int j) {
//定义两个变量记录要查找的范围
int start = i;
int end = j;
if(start > end){
//递归的出口
return;
}
//记录基准数
int baseNumber = arr[i];
//利用循环找到要交换的数字
while(start != end){
//利用end,从后往前开始找,找比基准数小的数字
//int[] arr = {1, 6, 2, 7, 9, 3, 4, 5, 10, 8};
while(true){
if(end <= start || arr[end] < baseNumber){
break;
}
end--;
}
System.out.println(end);
//利用start,从前往后找,找比基准数大的数字
while(true){
if(end <= start || arr[start] > baseNumber){
break;
}
start++;
}
//把end和start指向的元素进行交换
int temp = arr[start];
arr[start] = arr[end];
arr[end] = temp;
}
//当start和end指向了同一个元素的时候,那么上面的循环就会结束
//表示已经找到了基准数在数组中应存入的位置
//基准数归位
//就是拿着这个范围中的第一个数字,跟start指向的元素进行交换
int temp = arr[i];
arr[i] = arr[start];
arr[start] = temp;
//确定6左边的范围,重复刚刚所做的事情
quickSort(arr,i,start - 1);
//确定6右边的范围,重复刚刚所做的事情
quickSort(arr,start + 1,j);
}
要注意的是:从0开始做基准就一定要先移动end,再移动start不然会出问题
Lambda表达式
与面向对象不同,Lambda表达式就是一种在意逻辑不在意对象的体现,可以用来简化代码。
此外,还可以省略:
- 参数类型可以省略
- 只有一个参数时,参数的括号也可以省;
- 方法体只有一行,大括号,分号,return都可以省;
Arrays.sort(arr, (o1,o2) -> o1 - o2);
综合练习:
练习一:
定义数组,存储人类对象,利用Arrays中的方法进行排序
要求:属性得有姓名,年龄,身高;按照年龄大小排序;若年龄相同,比较身高,身高相同,按照姓名首字母(姓名暂时用拼音代替);
public static void main(String[] args) {
Person person1 = new Person("xiaozhang", 18, 1.66);
Person person2 = new Person("xiaowang", 18, 1.76);
Person person3 = new Person("xiaozhao", 19, 1.86);
Person person4 = new Person("az", 19, 1.86);
Person[] people = {person1, person2, person3, person4};
Arrays.sort(people, new Comparator<Person>() {
@Override
public int compare(Person o1, Person o2) {
double temp = o1.getAge() - o2.getAge();
//年龄相同比身高
temp = temp == 0 ? o1.getHeight() - o2.getHeight() : temp;
//身高相同比姓名首字母
temp = temp == 0 ? o1.getName().compareTo(o2.getName()) : temp;
if (temp > 0) {
return 1;
} else if (temp < 0) {
return -1;
} else {
return 0;
}
}
});
for (int i = 0; i < people.length; i++) {
System.out.println(people[i].getName());
}
}
}
class Person {
private String name;
private int age;
private double 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 double getHeight() {
return height;
}
public void setHeight(double height) {
this.height = height;
}
public Person() {
}
public Person(String name, int age, double height) {
this.name = name;
this.age = age;
this.height = height;
}
练习二:
求第十二个斐波那契数列的值(递归的方法):
public static void main(String[] args) {
//第一个月,1
//第二个月,1
//第三个月,2
//第四个月,3
//第五个月,5
//第五个月,8
System.out.println(getSum(12));
}
public static int getSum(int month) {
if (month ==1 || month == 2){
return 1;
}
return getSum(month-1)+getSum(month-2);
}
练习三:
一堆桃子,猴子第一天吃了一半后,又吃了一个 ;之后每天都这样,然后吃到第十天没吃呢,就只剩一个了,问最初共有多少桃子?
public static void main(String[] args) {
//我这个思路是把第十天视作第一天,即第一天已知
//然后去推第十天
System.out.println(getSum(10));
}
public static int getSum(int day) {
if (day == 1) {
return 1;
}
return (getSum(day - 1) + 1) * 2;
}
练习四:
某人去爬楼梯,有时他走两个台阶,有时他走一个台阶,若楼梯总长20阶,问他有几种爬楼梯方法?(去掉了一个1的斐波那契数列)
public static void main(String[] args) {
System.out.println(getSum(20));
}
public static int getSum(int count) {
if (count == 1) {
return 1;
}
if (count== 2){
return 2;
}
return getSum(count-1)+getSum(count-2);
}