算法图解
第一章 算法简介
1.1二分查找
输入需要是一个有序的元素列表。如果要查找的元素包含在列表中,二分查找返回其位置;否则返回null
一般而言,对于包含n个元素的列表,用二分查找最多需要log(n)步,而简单查找最多需要n步。【注:此处的log指的是以2为底,n为参数的对数运算】
函数binary_search接受一个有序数组和一个元素。如果指定的元素包含在数组中,这个函数将返回其位置。你将跟踪要在其中查找的数组部分——开始时为整个数组。
查询方法
private static int search(int[] array,int find){
//定义开始位置
int begin=0;
//定义结束的位置
int end=array.length-1;
//当开始位置在结束位置左侧时
while(begin<=end){
//找到中间位置
int mid=(begin+end)/2;
//根据索引找到对应的在数组中的元素
int res=array[mid];
if (res==find){
return mid; //如果需要查询的元素与mid索引照应的元素相同,则返回索引
}else if(res>find){
end=mid-1; //猜的数字大了,数在前半部分
}else {
begin=mid+1;//猜的数字小了,数在后半部分
}
}
return -1;
}
print输出结果方法
private static void print(int[] array, int index, int item) {
if (index == -1)
System.out.println("数组 "+ Arrays.toString(array) +" 中没有 " + item + " 这个元素");
else
System.out.println("数组 "+ Arrays.toString(array) +" 中存在 " + item + " 这个元素,它的索引为:" + index);
}
main方法测试
public static void main(String[] args) {
int[] array = {1,2,3,4,5};
int item = 4;
int index = search(array, item);
print(array, index, item);
int item1 = 7;
int index1 = search(array, item1);
print(array, index1, item1);
}
运行结果如下图所示
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mr0nae5Y-1644745968463)(C:\Users\Zty\AppData\Roaming\Typora\typora-user-images\image-20220212155803452.png)]
1.2大O表示法
大O表示法是一种特殊的表示法,指出了算法的速度有多快
假设列表包含n个元素。简单查找需要检查每个元素,因此需要执行n次操作。使用大O表示法,这个运行时间为O(n),但是没有单位。大O表示法让你能够比较操作数,它指出了算法运行时间的增速。
注意:大O表示法说的是最糟的情形,也可以理解为最大消耗的时间,而不是说某一种理想情况下的最佳值。
下面按从快到慢的顺序列出了我们经常会遇到的5种大O运行时间。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-czvF6VIl-1644745968465)(C:\Users\Zty\AppData\Roaming\Typora\typora-user-images\image-20220212160352113.png)]
1.3小节
❑ 二分查找的速度比简单查找快得多。
❑ O(log n)比O(n)快。需要搜索的元素越多,前者比后者就快得越多。
❑ 算法运行时间并不以秒为单位。
❑ 算法运行时间是从其增速的角度度量的。
❑ 算法运行时间用大O表示法表示。
第二章 选择排序
2.1数组和链表
数组占用的是一块连续的内存区,而链表在内存中是分散的,链表的每个元素都存储了下一个元素的地址,从而使一系列随机的内存地址串在一起。
数组支持“随机访问”,即访问其中任意一个元素所消耗的时间是相同的。但是对于链表就不是了,链表只能通过头节点指针,从每一个节点,依次往下找,如果要想访问List(10000),不得不通过前面的9999个节点;访问所需的时间复杂度分别是O(1)与O(n),方式一种是“随机访问”,一种是“顺序访问”。
2.2选择排序
public class _02选择排序test {
private static void SelectSort(int[] array){
//for循环遍历每一个元素
for (int i=0;i<array.length-1;i++) {
//默认最小值为第i个元素
int smallest=array[i];
//对应的索引
int index=i;
//.从i对应的数之后开始遍历数组
for (int j=i+1;j<array.length;j++){
// 判断当前遍历的元素是否比定义最小的元素小,用此方式求出最小值
if(array[j]<smallest){
smallest=array[j];
index=j;
}
}
//互换位置
int temp=array[i];
array[i]=smallest;
array[index]=temp;
}
}
public static void main(String[] args) {
int[] array = {5,3,2,1,4};
SelectSort(array);
System.out.println(Arrays.toString(array));
}
}
2.3小节
❑ 数组的元素都在一起。
❑ 链表的元素是分开的,其中每个元素都存储了下一个元素的地址。
❑ 数组的读取速度很快。
❑ 链表的插入和删除速度很快。
第三章 递归
3.1递归
如果使用循环,程序的性能可能更高;如果使用递归,程序可能更容易理解
递归与for循环对比:
public class _03递归for {
public static void main(String[] args) {
for (int i = 9; i > 0; i--) {
System.out.print(i);
}
}
}
输出结果:
987654321
public class _03递归 {
public static void main(String[] args) {
recursion(9);
}
private static void recursion(int n) {
// 递归终止条件,也就是递归的出口
if (n == 0){
return ;
} else {
//递归调用
recursion(n - 1);
// 输出当前递归的数
System.out.println(n);
}
}
}
输出结果:
123456789
原理:递归是在栈中执行的,栈是一种先进后出的数据结构,方法先要入栈,然后再出栈。
3.2基线条件和递归条件
由于递归函数调用自己,因此编写这样的函数时很容易出错,进而导致无限循环。
编写递归函数时,必须告诉它何时停止递归。正因为如此,每个递归函数都有两部分:基线条件(base case)和递归条件(recursive case)。递归条件指的是函数调用自己,而基线条件则指的是函数不再调用自己,从而避免形成无限循环。
基线条件可以理解为结束递归或者跳出递归的条件,递归条件即满足继续执行函数调用自己的条件。
3.3栈
调用另一个函数时,当前函数暂停并处于未完成状态。该函数的所有变量的值都还在内存中。
栈包含未完成的函数调用,每个函数调用都包含还未完成的函数。使用栈很方便,因为我们无需自己跟踪函数堆——栈完成了这项工作。
使用栈虽然很方便,但是也要付出代价:存储详尽的信息可能占用大量的内存。每个函数调用都要占用一定的内存,如果栈很高,就意味着计算机存储了大量函数调用的信息。在这种情况下,你有两种选择:
❑ 重新编写代码,转而使用循环。
❑ 使用尾递归。
假设你编写了一个递归函数,但不小心导致它没完没了地运行。正如你看到的,对于每次函数调用,计算机都将为其在栈中分配内存。递归函数没完没了地运行时,将给栈带来什么影响?
答:调用栈不断加层,会越来越长,直至计算机内存不够。
3.4小节
❑ 递归指的是调用自己的函数。
❑ 每个递归函数都有两个条件:基线条件和递归条件。
❑ 栈有两种操作:压入和弹出。
❑ 所有函数调用都进入调用栈。
❑ 调用栈可能很长,这将占用大量的内存。
第四章 快速排序
4.1分而治之
分而治之(divide and conquer, D&C)——一种著名的递归式问题解决方法。
D&C的工作原理:
(1) 找出简单的基线条件;
(2) 确定如何缩小问题的规模,使其符合基线条件。
编写涉及数组的递归函数时,基线条件通常是数组为空或只包含一个元素。陷入困境时,请检查基线条件是不是这样的。
递归求和
public class _03递归求和 {
public static void main(String[] args) {
int[] arr = {2, 4, 6};
// 用递归求数组之和
int sum = recursiongetsum(arr, 0);
System.out.println(Arrays.toString(arr) + "中各数字之和为:" + sum);
}
private static int recursiongetsum(int[] arr, int index) {
if (index == (arr.length - 1)) {
return arr[index];
}else {
return arr[index] + recursiongetsum(arr, index+1);
}
}
}
4.2快速排序
如何对包含三个元素的数组进行排序了,步骤如下:
(1) 选择基准值。
(2) 将数组分成两个子数组:小于基准值的元素和大于基准值的元素。
(3) 对这两个子数组进行快速排序
归纳证明----一种推理证明方式
归纳证明是一种证明算法行之有效的方式,它分两步:基线条件和归纳条件。是不是有点似曾相识的感觉?例如,假设我要证明我能爬到梯子的最上面。递归条件是这样的:如果我站在一个横档上,就能将脚放到上一个横档上。换言之,如果我站在第二个横档上,就能爬到第三个横档。这就是归纳条件。而基线条件是这样的,即我已经站在第一个横档上。因此,通过每次爬一个横档,我就能爬到梯子最顶端。
对于快速排序,可使用类似的推理。在基线条件中,我证明这种算法对空数组或包含一个元素的数组管用。在归纳条件中,我证明如果快速排序对包含一个元素的数组管用,对包含两个元素的数组也将管用;如果它对包含两个元素的数组管用,对包含三个元素的数组也将管用,以此类推。因此,我可以说,快速排序对任何长度的数组都管用。它很有趣,并与D&C协同发挥作用。
快速排序(java)参考:(52条消息) 快速排序(java实现)_王玉Student的博客-CSDN博客_java快速排序
public class _04快速排序 {
public static void quickSort(int[] a,int left,int right){
//分别创建i哨兵、j哨兵、temp存储基准值的临时变量、t交换的临时变量
int i,j,temp,t;
//终止递归,终止排序的条件(此时已经是排好的顺序)
if (left > right) {
return;
}
//分别用i、j存储两个哨兵
i = left;
j = right;
//temp存储的就是基准值
temp = a[left];
//跳出while循环之后,因为循环的条件是i<j,所以,跳出循环时,i和j是相等的
while (i < j) {
//哨兵j从右往左找
while (temp <= a[j] && i < j) {
j--;
}
while (temp >= a[i] && i < j) {
i++;
}
//交换两个数在数组中的位置
if (i < j) {//当哨兵i和哨兵j没有相遇时
t = a[j];
a[j] = a[i];
a[i] = t;
}
}
//最终基准值归位
a[left] = a[i];
a[i] = temp;
quickSort(a, left, j - 1);//继续处理左边的,这里是递归的过程
quickSort(a, j + 1, right);//继续处理右边的,这里是递归的过程
}
public static void main(String[] args) {
int[] arr = new int[] {3,1,2,5,4};
quickSort(arr, 0, arr.length - 1);
System.out.println(Arrays.toString(arr));
}
}
4.3再谈大O
选择排序,其运行时间为O(n2),速度非常慢。
还有一种名为合并排序(merge sort)的排序算法,其运行时间为O(n log n),比选择排序快得多!快速排序的情况比较棘手,在最糟情况下,其运行时间为O(n2)【此处为n的平方】。
在平均情况下,快速排序的运行时间为O(n log n)。你可能会
比较合并排序和快速排序
快速排序的性能高度依赖于你选择的基准值***。假设你总是将第一个元素用作基准值,且要处理的数组是有序的。由于快速排序算法不检查输入数组是否有序,因此它依然尝试对其进行排序。此时数组并没有被分成两半,相反,其中一个子数组始终为空,这导致调用栈非常长*。现在假设你总是将中间的元素用作基准值,在这种情况下,调用栈短得多!因为你每次都将数组分成两半,所以不需要那么多递归调用。你很快就到达了基线条件,因此调用栈短得多。第一个示例展示的是最糟情况,而第二个示例展示的是最佳情况。在最糟情况下,栈长为***O(n)***,而在最佳情况下,栈长为***O(logn)***。
最佳情况也是平均情况。只要你每次都随机地选择一个数组元素作为基准值,快速排序的平均运行时间就将为O(n log n)。快速排序是最快的排序算法之一,也是D&C典范。
4.4小节
❑ D&C将问题逐步分解。使用D&C处理列表时,基线条件很可能是空数组或只包含一个元素的数组。
❑ 实现快速排序时,请随机地选择用作基准值的元素。快速排序的平均运行时间为O(n log n)。
❑ 大O表示法中的常量有时候事关重大,这就是快速排序比合并排序快的原因所在。
❑ 比较简单查找和二分查找时,常量几乎无关紧要,因为列表很长时,O(log n)的速度比O(n)快得多。
因此调用栈短得多。第一个示例展示的是最糟情况,而第二个示例展示的是最佳情况。在最糟情况下,栈长为***O(n)***,而在最佳情况下,栈长为***O(logn)***。
最佳情况也是平均情况。只要你每次都随机地选择一个数组元素作为基准值,快速排序的平均运行时间就将为O(n log n)。快速排序是最快的排序算法之一,也是D&C典范。