第一章复杂度
1.1 大O记法
我们都知道大部分程序基本都在处理 获取 查询 操作和返回数据相关的逻辑,我们数据的结构和算法就是为了更快,更省的进行数据处理。
更快(时间少):时间复杂度
更省(内存少):空间复杂度
为什么需要复杂度?如果只用开始时间和结束时间之间的差额和内存使用情况来看会有什么弊端?
1 测试结果大大依赖硬件条件
2 测试结果需要事后计算
3 测试结果受到原始数据特性影响大
所以我们需要一个不需要具体的测试环境就可以粗略估计算法执行效率的方法这就是复杂度又叫大0记法
大O记法是算法的基石和精髓,只有学好大O记法,我们才能掌握好数据的结构和算法
1.2 时间复杂度
如何计算时间复杂度?
我们把步数作为时间复杂度的单位:数组每次索引值的读取就算作一步 unit_time
常数时间 O(1)例子 计算机获取数组的某一个值
对数时间 O(log(N))
线性时间 O(N) 一般一个for循环会被视为N
指数时间 O(N^2)for循环嵌套一个for循环
一般来说 指数>线性>对数>常数
1.3 空间复杂度
以基础数据类型值作为空间复杂度的一个单位
一般来说我们不考虑空间复杂度,除非参与嵌入式开发或者单片机的编程开发。因为我们内存空间提升非常快 在价格不变情况下,每隔18到24个月,同样
大小的芯片数据储存量就能翻一倍
1.4 二分法查找数据
如果查找一个有序数组中的某一个数字,我们有两种方法:直接遍历查找和使用二分法
直接遍历:一个for循环 空间复杂度 O(N)
二分法:
public static int find(int[] array, int aim) {
// 初始化left = 最左侧, right = 最右侧
int left = 0;
int right = array.length - 1;
// 当left > right则表示遍历完成
while (left <= right) {
// 获取中间的值
int middle = (left + right) / 2;
int middleValue = array[middle];
if (middleValue == aim) {
// 已经找到目标
return middle;
} else if (middleValue < aim) {
// 目标在middle的右侧,重置left
left = middle + 1;
} else {
// 目标在middle的左侧,重置right
right = middle - 1;
}
}
return -1;
}
当数组长度越大时,二分法的性能提升越明显 例如有100万个数字 直接遍历需要100万次而二分法只需要19.93次
1.5 判断重复问题
假设一个数组里面有数字(0到10)重复了,如果要求你把它找出来我们也有两种方法:直接遍历判断和表记法
直接遍历2次,看第一个数字是否和后面数字相同,然后看第二个,第三个,
空间复杂度为O(N^2)
public static ArrayList<Integer> repeat(int[] array) {
ArrayList<Integer> result = new ArrayList<>();
for(int i = 0; i < array.length; i++){
// 以此判断i位置元素和后面j位置元素是否相等
for(int j = i + 1; j < array.length; j++){
if(array[i] == array[j]){
result.add(array[i]);
}
}
}
return result;
}
标记法:我们可以找一个空的11个位置的数组b,遍历原数组a,a出现一个值,就将b对应索引位置的值由0变为1,
当a后面再次出现这个值时,b对应索引位置的值已经是1了,表示已经重复
时间复杂度O(N)
public static ArrayList<Integer> repeat(int[] array) {
ArrayList<Integer> result = new ArrayList<>();
int[] exists = new int[11];
for (int i = 0; i < array.length; i++) {
int value = array[i];
// 如果当前位置已经为1,则表示重复
if (exists[value] == 1) {
result.add(value);
} else {
// 否则将当前位置设置为1
exists[value] = 1;
}
}
return result;
}
第二章 数组
2.1 计算机内存管理
内存:计算机程序运行的地方
内存里面有许多内存单元,每个内存单元都有自己的内存地址(相差8)
2.2 数组的存储和读取
数组索引为什么是从0开始的呢?
数组存储:数组是一种线性表数据结构,它用一组连续的内存空间,来存储一组数据类型相同的数据
数组读取:
// 第一个元素地址
start_address+ item_size * 0 //为了方便计算,数组的索引就从0开始
// 第二个元素地址
start_address + item_size * 1
// 第三个元素地址
start_address + item_size * 2
// 第N个元素地址
start_address + item_size * (N - 1)
由此可以看出数组的时间复杂度为O(1)
2.3 数组的插入和删除
数组插入:
尾部插入
public class Schedule {
......
// 末尾插入
public void add(String task){
this.array[this.size] = task;
this.size++;
}
}
中间插入
为了保证数组的顺序和连续的内存空间,我们需要把插入位置后面的数据一次向后面移动
例子:在第三个位置插入数据
// 第三个位置插入
public void insert3Position(String task) {
// 索引值为3的地方
int index = 3;
// 第一步:从右侧开始依次右移
for (int i = this.size - 1; i >= index; i--) { //自减
this.array[i + 1] = this.array[i]; //数据向右移动一个位置
}
// 第二部:插入元素
this.array[index] = task;
// 调整size
this.size++;
}
如果空间不够,系统就会抛出异常 Out Of Memory表示内存不够了
数组删除 删除操作和插入操作正好相反
中间删除 i自加 ,数据依次向左移动一个位置
第三章 入门排序算法
3.1 冒泡排序
对于一个数组,每一次循环都使最大的值冒泡到最后面
核心规则:1 指向数组中两个相邻的元素,并比较他们的大小
2 如果前者比后者大,就交换他们的位置。反之,不交换
3 然后依次后移,将最大的值移动到最后面
例子
// 冒泡排序
public static void bubbleSort(int[] array) {
// 1. 每次循环,都能冒泡出剩余元素中最大的元素,因此需要循环 array.length 次
for (int i = 0; i < array.length; i++) {
// 2. 每次遍历,只需要遍历 0 到 array.length - i - 1中元素,因此之后的元素都已经是最大的了
for (int j = 0; j < array.length - i - 1; j++) {
//3. 交换元素
if (array[j] > array[j + 1]) {
int temp = array[j + 1];
array[j + 1] = array[j];
array[j] = temp;
}
}
}
}
时间复杂度:for循环嵌套for循环 O(N^2)
3.2 选择排序
每次在剩余数组中,选择最大的或者最小的一个数据,放在数组的某一端
这里以选择最大放右边为例
核心规则:1利用两个变量,一个储存最大值,另一个储存最大值对应的索引
2依次比较后面的数字,如果比当前变量储存的值大,就更新最大值和最大值对应的索引
3遍历结束后交换当前最大值和最右边位置的值
4重复上面步骤
时间复杂度:O(N^2)
选择排序比冒泡排序快了一倍:冒泡排序需要频繁交换数据,而选择排序只需要交换一次
import java.util.Arrays;
public class Sort {
// 选择排序
public static void selectSort(int[] array) {
for (int i = 0; i < array.length; i++) {
int a = array[0];
int b = 0;
for (int j = 0; j < array.length - 1 - i; j++) {
if (array[j + 1] > a) {
a = array[j + 1]; //最大值
b = j + 1;
}
}
int c = array.length - 1 - i; //最右边元素的下标
int d = a; //d是中间元素
array[b] = array[c];
array[c] = d;
}
}
public static void main(String[] args) {
int[] array = {
9,
2,
4,
7,
5,
3
};
// Arrays.toString 可以方便打印数组内容
System.out.println(Arrays.toString(array));
selectSort(array);
System.out.println(Arrays.toString(array));
}
}
###3.3 插入排序
重点在于插入,每次抽离一个元素作为临时元素,依次比较和移动后面的元素,最终将这个元素插入合适的位置
以倒数第二个元素为临时元素为例
核心规则 : 1 抽离数组的倒数第二个元素作为临时元素
2 用临时元素与数组后面的元素进行对比,如果后面的元素值小于临时元素则左移
3 如果后面的元素大于临时元素,或者已经移动到数组的最右边则将临时元素插入当前空隙中
4 重复上面的步骤
将部分元素先排好,最后再排在一起
时间复杂度:最好的情况O(N)
最坏的情况 O(N^2)
在实际情况中,我们要比较数据的原始特性,如果很大一部分已经排好了,我们就用插入排序 如果很乱,我们使用选择排序,一般不用冒泡排序。
例子
import java.util.Arrays;
public class Sort {
// 插入排序
public static void insertSort(int[] array) {
// 从倒数第二位开始,遍历到底0位,遍历 N-1 次
for (int i = array.length - 2; i >= 0; i--) {
// 存储当前抽离的元素
int temp = array[i];
// 从抽离元素的右侧开始遍历
int j = i + 1;
while (j <= array.length - 1) {
// 如果某个元素,小于临时元素,则这个元素左移
if (array[j] < temp) {
array[j - 1] = array[j];
} else {
// 如果大于,则直接将临时元素插入,然后退出循环。
array[j - 1] = temp;
break;
}
j++;
}
// 处理到达尾部的情况
if (j == array.length) {
array[j - 1] = temp;
}
}
}
public static void main(String[] args) {
int[] array = {9, 2, 4, 7, 5, 3};
// Arrays.toString 可以方便打印数组内容
System.out.println(Arrays.toString(array));
insertSort(array);
System.out.println(Arrays.toString(array));
}
}
3.4 插入排序的进阶:二分插入排序
对于有序列表我们可以采用二分法查找位置,在插入排序中我们同样也可以使用二分法
import java.util.Arrays;
public class Sort {
// 查找应该插入的索引位置
public static int searchIndex(int[] array, int left, int right, int aim) {
// 循环查找节点位置
while (left < right) {
int middle = (left + right) / 2;
int value = array[middle];
if (value < aim) {
left = middle + 1;
} else {
right = middle - 1;
}
}
// 如果最终元素仍然大于目标元素,则将索引位置往左边移动一个
if(array[left] > aim){
return left -1;
}
// 否则就是当前位置
return left;
}
// 插入排序
public static void insertSort(int[] array) {
// 从倒数第二位开始,遍历到底0位,遍历 N-1 次
for (int i = array.length - 2; i >= 0; i--) {
// 存储当前抽离的元素
int temp = array[i];
int index = searchIndex(array, i + 1, array.length - 1, temp);
// 根据插入的索引位置,进行数组的移动和插入
int j = i + 1;
while (j <= index) {
array[j - 1] = array[j];
j++;
}
array[j - 1] = temp;
}
}
public static void main(String[] args) {
int[] array = {9, 2, 4, 7, 5, 3};
// Arrays.toString 可以方便打印数组内容
System.out.println(Arrays.toString(array));
insertSort(array);
System.out.println(Arrays.toString(array));
}
}
第四章 递归
4.1 什么是递归,递归可以解决什么问题?
递归在编程中体现为:函数自己调用自己
递归在数学中的应用:
阶乘
n! = 1 * 2 * 3 * 4 * …… * (n-1) * n
循环实现
public static int factorial(int n) {
int factorial = 1;
for (int i = 1; i <= n; i++) {
factorial *= i;
}
return factorial;
}
递归实现
f(1) = 1
f(n) = f(n-1) * n
public static int factorial(int n) {
//#1. 当 n = 1 时,递归结束
if (n == 1) {
return 1;
}
//#2. 把 factorial(n - 1) 的结果和 n 相乘,剩下的交给 factorial(n - 1) 来解决。
return n * factorial(n - 1);
}
汉诺塔问题作为递归的经典案例,可以去了解一下
4.2 递归应该如何实现
1 基准条件:递归结束语句
没有基准条件,函数将陷入死循环
2 递归公式
总结:1 先找出基准条件
2 思考在基准条件下,会出现什么情况
3 思考基准条件前一步的情况,或者函数执行情况
4 配合递归公式,继续向前推进
5 最后实现完整的代码
4.3 分而治之思想–归并排序
分而治之思想:将原问题分解为几个规模较小但是类似于原始问题的问题,然后用递归解决这些小问题,最后把这些小问题的解合并起来组成原问题的解
归并排序的核心思路:将大数组分解为小数组,然后把小数组排好序,再将这些有序的小数组合并为大数组
分代码实现: 1 如何递归进行数组的拆分
2 如何用原始数组创建两个子数组
例子:将数组 {9, 2, 4, 7, 5, 3} 进行归并排序 时间复杂度为O(Nlog(N)) 速度稳定,没有最好最差的情况
import java.util.Arrays;
public class Sort {
// 归并排序
public static int[] mergeSort(int[] array) {
// 为了方便查看结果,我们将每个数组进行打印
if (array.length == 1) {
return array;
}
int middle = array.length / 2;
// 处理 0 到 middle 左侧数组部分
int[] left = mergeSort(subArray(array, 0, middle));
// 处理 middle 到 array.length 右侧数组部分
int[] right = mergeSort(subArray(array, middle, array.length));
// TODO处理合并问题
int l = 0;
int r = 0;
int index = 0;
// 依次比较左右两个数组
while (l < left.length && r < right.length) {
array[index] = Math.min(left[l], right[r]);
index++;
if (left[l] < right[r]) {
l++;
} else {
r++;
}
}
// 右侧数组已经遍历完成,左侧有剩余
if (l < left.length) {
for(int i = l; i < left.length; i++){
array[index] = left[i];
index++;
}
}
// 左侧数组已经遍历完成,右侧有剩余
if(r < right.length){
for(int i = r; i < right.length; i++){
array[index] = right[i];
index++;
}
}
return array;
}
// 拷贝原数组的部分内容,从 left 到 right
public static int[] subArray(int[] source, int left, int right) {
// 创建一个新数组
int[] result = new int[right - left];
// 依次赋值进去
for (int i = left; i < right; i++) {
result[i - left] = source[i];
}
return result;
}
public static void main(String[] args) {
int[] array = {9, 2, 4, 7, 5, 3};
// Arrays.toString 可以方便打印数组内容
System.out.println("raw: " + Arrays.toString(array));
int[] result = mergeSort(array);
System.out.println("result: " + Arrays.toString(result));
}
}
4.4 分而治之思想 快速排序
快速排序是我们使用最多的算法,也是面试时提问最多的算法
以从小到大排序为例子
实现原理:我们随机选择一个数字作为轴,将原始数组进行拆分,将比轴大的数字放在轴右边,将比轴小的数字放在左边
给定一个数组1,6,4,2,5,3 我们来理一下逻辑
选3为轴,索引为0为左指针,倒数第二个为右指针,将左指针向右移动遇到比3大停,这里到6停。
右指针向左移动,遇到比3小的数字停,这里到2停。交换它们的位置,现在数组为124653 左指
针继续右移动,到4停下,右指针也移动到了4,交换4和3的位置。现在数组为123654,我们已经
将数组分为了小于3和大于3的两个部分 然后我们在这两个部分里面重复上面步骤选取2和4为轴
快速排序的基准条件:当数组元素小于或者等于1的时候结束
递归公式:每次分区后,获取到轴的位置后,左右拆分数组,继续快速排序
它的时间复杂度O(Nlog(N)),如果时已经排列好的那么时间复杂度为O(N^2)
例子 以数组{9, 2, 4, 7, 5, 3}进行快速排序
import java.util.Arrays;
public class QuickSort {
// 快速排序
public static void quickSort(int[] array) {
// 调用快速排序的核心,传入left,right
quickSortCore(array, 0, array.length - 1);
}
// 快速排序的核心,同样也是递归函数
public static void quickSortCore(int[] array, int left, int right) {
// 递归基准条件,left >= right 即表示数组只有1个或者0个元素。
if (left >= right) {
return;
}
// 根据轴分区
int pivotIndex = partition(array, left, right);
// 递归调用左侧和右侧数组分区
quickSortCore(array, left, pivotIndex - 1);
quickSortCore(array, pivotIndex + 1, right);
}
// 对数组进行分区,并返回当前轴所在的位置
public static int partition(int[] array, int left, int right) {
int pivot = array[right];
int leftIndex = left;
int rightIndex = right - 1;
while (true) {
// 左指针移动
while (array[leftIndex] <= pivot && leftIndex < right) {
leftIndex++;
}
// 右指针移动
while (array[rightIndex] >= pivot && rightIndex > 0) {
rightIndex--;
}
if (leftIndex >= rightIndex) {
break;
} else {
swap(array, leftIndex, rightIndex);
}
}
swap(array, leftIndex, right);
return leftIndex;
}
public static void swap(int[] array, int index1, int index2) {
int temp = array[index1];
array[index1] = array[index2];
array[index2] = temp;
}
public static void main(String[] args) {
int[] array = {9, 2, 4, 7, 5, 3};
// Arrays.toString 可以方便打印数组内容
System.out.println("raw: " + Arrays.toString(array));
quickSort(array);
System.out.println("result: " + Arrays.toString(array));
}
}
4.5 快速排序应用–快速选择
时间复杂度O(N)
原理:当我们找某个数组中第几大的数字时,假设数组长度为10的数组找第4大数字。我们第一次分区后,
如果右边区域大于4我们就可以只取右边区,然后继续分,继续抛取,直到找到目标数字
例子
import java.util.Arrays;
public class QuickSort {
// 快速选择,返回选中的元素
public static int quickFind(int[] array, int aim) {
// 调用快速选择的核心,传入left,right
return quickFindCore(array, aim, 0, array.length - 1);
}
// 快速选择的核心,同样也是递归函数
public static int quickFindCore(int[] array, int aim, int left, int right) {
// 递归基准条件,left >= right 即表示数组只有1个或者0个元素,返回当前的元素
if (left >= right) {
return array[left];
}
// 根据轴分区
int pivotIndex = partition(array, left, right);
// 根据 aim 确定继续递归的方向
if (pivotIndex > aim) {
return quickFindCore(array, aim, left, pivotIndex - 1);
} else if (pivotIndex < aim) {
return quickFindCore(array, aim, pivotIndex + 1, right);
} else {
return array[pivotIndex];
}
}
// 对数组进行分区,并返回当前轴所在的位置
public static int partition(int[] array, int left, int right) {
int pivot = array[right];
int leftIndex = left;
int rightIndex = right - 1;
while (true) {
// 左指针移动
while (array[leftIndex] < pivot && leftIndex < right) {
leftIndex++;
}
// 右指针移动
while (array[rightIndex] > pivot && rightIndex > 0) {
rightIndex--;
}
if (leftIndex >= rightIndex) {
break;
} else {
swap(array, leftIndex, rightIndex);
}
}
swap(array, leftIndex, right);
return leftIndex;
}
public static void swap(int[] array, int index1, int index2) {
int temp = array[index1];
array[index1] = array[index2];
array[index2] = temp;
}
public static void main(String[] args) {
int[] array = {72, 77, 48, 17, 71, 2, 25, 97, 82, 5, 2, 18, 15, 57, 7, 48, 93, 47, 38, 74, 18, 93, 98, 41, 54, 4, 47, 4, 63, 76};
System.out.println("raw: " + Arrays.toString(array));
// 目标是倒数第 6 个元素
int result = quickFind(array, array.length - 6);
System.out.println("result: " + result);
}
}