引言
效率=产出/所做的事情
提高效率的本质: 让计算机少做事情
在边界内做事情:从数学上可以证明N个任意随机数的排序,复杂度不可能比N乘以log(N)更低,这是数学给出的极限(边界)。
I 预备知识
1.1 计算机科学的基本做事原则:比较标准
明确一个公平的比较标准:过滤掉所有次要的因素,构建一个理想的环境(虚拟的环境),构建可比较的、容易对比的、理想的平台。
应用场景:首先明确游戏规则,然后再开始做研究,确定基线,对比方法的优缺点。
将世界理想化的目的:找到真正的主要矛盾,过滤出那些相对次要的噪音。 将问题抽象出来,以便于抓住本质。
案例:
- 伽利略和牛顿在研究运动时,假设空气阻力和摩擦力可以忽略不计的,来构建理想状态。
- 科学家波义耳和马略特在研究气体压强和空间大小的关系时,也是先构造出理想的气体状态。
- 亚里士多德因为无滤除空气阻力对重力加速度的影响,得到了重物比轻物下降速度快的荒唐结论。
1.2 计算机思维的本质
翻译
把人想要做的具体事情,翻译成计算机能够懂得的程序语言。 软件开发中的关键任务就是理解并处理反映软件构成的复杂概念。
1.3 量级思维
数量级指一系列 10 的幂:数量级每差出一级,数据相差十倍左右。
个、十、百、千、万
量级是一种方便表示数量级的方式,通常使用科学计数法表示。
地球的质量是5.98x
10^24
千克,其中10^24
就是量级。 量级简单地讲,就是芝麻、橘子、西瓜、大象、大山、地球、太阳、银河系这样大的差别。
一个人在公司的成就:成功率x事情的量级x做事的速度
提高事情的量级需要不断学习,转变自己的角色。
从老师转型成校长,就有了量级上的突破。
对不同规模的问题要采用不同的方法
1.4 合格的工程师处理不同量级事物时的做法
- 考虑量级的差异: 小量级和大量级的东西放在一起,前者必须被忽略掉。
- 几个小量级的东西放在一起,远比不上一个大量级的东西。
- 工程师要懂得从量级上改进工程方法,这样的收获几万倍,甚至更多。
- 工程师要能够梳理出一个难题中各个因素在量级上的不同,知道把那些无关紧要的事情从自己的To Do List上删掉。
- 工程师要想着在更有影响力的事情中,参与1%。
- 产品经理要想怎样能让用户为你的产品多掏一倍的价钱,懂得在细节上做1%的改进,让产品的品质显得高出一个数量级。
Mac的视网膜显示屏成本比一般的显示屏价格贵不了10美元,但可以让电脑多卖一百多美元,而且用户的感觉好了不止一倍。
- 投资人要想办法写出最大的一张支票。
- 讲师要想如何当好校长。
科学家考虑的是对和错,工程师只是在现有条件下考虑好和坏的解决方案。
一个合格的软件工程师,要知道采用别人已经写好的,被长期证明运行无误的源代码,作为自己开发新产品的工具,这样才能把精力集中在解决前人未解决的问题上。
科学家告诉大家这件事的可行性,但工程师要明白怎么做。
工程师的工作:科学变成技术再变成产品
II 计算机算法
2.1 定义
计算机算法:在翻译现实世界的需求和计算机虚拟过程时,提炼出一些高效的、不断被验证过的标准流程。
将现实世界的这些问题,变成计算机可以运行的程序,中间的桥梁就是算法。 算法是一组用于解决问题或完成任务的指令或规则集合。 算法是解决问题的明确步骤,通常涉及数据操作、逻辑和控制结构。 它们是计算机程序中最基本的构建块之一,用于指导计算机执行特定的任务或操作。
2.2 模块化
制作几个非常简单,能够大量复制的模块,搭出复杂的系统。
在软件中,那些模块就是一个个的算法。
- 将复杂的问题简单化
- 降低成本,提高效率。
2.3 衡量算法的标准:算法复杂度
高德纳的思想
- 在比较算法的快慢时,需要考虑数据量特别特别大,大到近乎无穷大时的情况。
计算机的发明就是为了处理大量数据的
- 决定算法快慢的因素虽然可能很多,但是所有的因素都可以被分为两类。第一类是不随数据量变化的因素,第二类是随着数量变化的。
在研究算法时,不必考虑不变的常数,只需要看变化的因素。 算法的速度主要由后面一个因素决定,随着数量变化的因素会造成量级的差异。
- 如果两种算法在量级上相当,在计算机科学里,就认为它们是一样好。
考虑量级的差异: 小量级和大量级的东西放在一起,前者必须被忽略掉。
高德纳删除了前面的常数因子,只保留后面的变量,他用了微积分中的一个概念——大写的O的概念,所有同等复杂度的算法,都被认为它们在"大O的概念"下是相等的。
比如冒泡排序算法的复杂度是O(N平方),插入排序也是O(N平方),属于同一个量级。
2.4 递归算法
- 递归的本质:自己调用自己
2)递归是一种算法,它的基本思想:将大事分解、从小事做起,步步干净利落、自顶向下设计,再自下而上回归。
对于一个复杂的问题,将原问题分解为若干个相对简单的子问题,继续下去直到简单的问题能够解决求解,也就是找到问题的出口。
3)关键因素:
- 递归的出口
- 递归逐步的向出口推进
4)递归的应用场景:当一次运算无法计算出结果,而且是自己调用自己,只有找到出口才拿到结果。然后返回来进行下一步的运算。
5)递归的特点:递归的时候,按照递归的深度分配全部的临时变量,栈开销很大,性能较低,要注意不能超越栈空间的大小。并且要给出一定的结束条件,否则,会造成栈溢出。6)例子:阶乘
代码语言:javascript复制public class Recursion{
public static void main(String[] args){
recursion(100);
}
public int recursion(int n){
if(n=1){
return ;
}
return n*recursion(n-1);
}
}
III 提高效率的本质:少做事情
效率=产出/所做的事情
提高效率的本质: 让计算机少做事情
在边界内做事情:从数学上可以证明N个任意随机数的排序,复杂度不可能比N乘以log(N)更低,这是数学给出的极限(边界)。
3.1 归并排序
归并排序利用了少做事情的思想,减少数据之间的相互比较,对冒泡排序进行改进。
原理:自顶向下细分,再自底向上合并。
将待排序的序列分成若干个子序列,每个子序列都是有序的,然后再将子序列间有序地合并成一个有序序列。
将一个复杂问题分解成很多简单的问题,一个个解决,最后比直接解决复杂问题要省很多时间。
- 冒泡排序的时间复杂度为
O(n^2)。
- 归并排序的时间复杂度为
O(nlogn)
。
3.2 快速排序
通常情况下最好的排序算法是"快速排序(Quicksort)"
算法,它是基于比较的排序算法,其时间复杂度平均情况下是 O(nlogn),最坏情况下是 O(n^2),工程师们会想一些方法防止极端糟糕情况的发生。
最好的计算机算法总是有附加条件,没有绝对的最好。 常情况下复杂度是N乘以log(N),和归并排序相同。根据计算机科学的标准,它们同样好。
在工程上,快速排序算法一般情况下比归并排序快两倍。
原理:快速排序还是强调少做事情
- 对于一大堆无序的数字,从中随机挑选一个,这个被随机选上的数字被称为
枢值
,接下来,将所有要排序的数字分成两部分,第一部分是大于等于枢值的,第二部分是小于枢值的。 - 从上面得到的两堆数字,分别采用第一步的方法各自再找一个枢值。接下来,再把两堆数字各自分成大于等于相应枢值的数字序列,以及小于枢值的数字序列。
- 用同样的方法,四堆变八堆,八堆变十六堆,很快所有的数字就排好序了。
应用:
- 快速挑选学生尖子:划出几个分数线,根据个人成绩的高低把学生分到十所学校去,第一所学校里的学生成绩最好,第十所最差,很容易地找出学习尖子。
当一个学校的学生水平都比较接近,老师教起来就容易,因此按照成绩对学生作一个初步的划分是有道理的,尤其在资源不足的情况下。
- 高效率的社会管理办法:对每一个人作一些区分。
私营公司行政的层级如同快速排序事先划定的枢值,有了三六九等,公司才有效率可言。 刻意追求所有人一律平等,不作区分,是效率最低的办法。
IV 排序
概念:让数据有序的存储
分类:选择,冒泡,插入,归并,快速和堆
通常情况下最好的排序算法是"快速排序(Quicksort)"
算法,它是基于比较的排序算法,其时间复杂度平均情况下是 O(nlogn),最坏情况下是 O(n^2)。
最好的计算机算法总是有附加条件,没有绝对的最好。
排序的应用场景:
- 给学生成绩排序,评奖学金或者推研
- 电商对销售根据一些选项排序来改进自己的业务
4.1 选择排序
时间复杂度为O(n^2)。
思路:每次找到未排序部分中的最小值,然后将其放到已排序部分的最后一个位置。
固定一个位置,与其他位置作比较,满足条件交换位置。
具体实现:使用两个嵌套的循环,外层循环用来控制已排序部分的长度,内层循环用来找到未排序部分中的最小值,并将其和已排序部分的最后一个位置进行交换。
代码语言:javascript复制public static void selectionSort(int[] arr) {
int n = arr.length;
for (int i = 0; i < n - 1; i ) {//控制已排序部分的长度
int minIndex = i;
for (int j = i 1; j < n; j ) {//找到未排序部分中的最小值,
if (arr[j] < arr[minIndex]) {
minIndex = j;
}
}
//将最小值和已排序部分的最后一个位置进行交换。
int temp = arr[minIndex];
arr[minIndex] = arr[i];
arr[i] = temp;
}
}
使用i表示第一个数据:0~arr.length-2
使用j表示后面部分的数据:i 1~arr.length-1;(j<arr.length,j ;i>j交换)
4.2 冒泡排序
冒泡排序算法的复杂度是O(N平方),插入排序也是O(N平方),属于同一个量级。
思想:从前往后比较相邻的两个元素,如果前一个元素比后一个元素大,则交换这两个元素,这样每一轮比较都会将当前未排序序列中的最大值放到序列末尾。
总是相邻的两个位置作比较,如果满足条件,交换位置。 每一次选出一个最好的,如同从水里冒出的气泡。
案例:成绩排序
第一次挑出成绩最高的同学,第二次挑出成绩次高的,如此重复。
Java 实现冒泡排序的代码:
外层循环控制排序轮数,内层循环用于比较相邻元素并交换位置。
代码语言:javascript复制public class BubbleSort {
public static void bubbleSort(int[] arr) {
if (arr == null || arr.length == 0) {
return;
}
int n = arr.length;
for (int i = 0; i < n - 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;
}
}
}
}
public static void main(String[] args) {
int[] arr = {3, 1, 5, 2, 4};
bubbleSort(arr);
for (int i : arr) {
System.out.print(i " ");
}
}
}
4.3 插入排序
基本思想:将待排序的数据分成两部分,已排序部分和未排序部分。每次从未排序部分取出一个元素,将其插入到已排序部分中的合适位置,直到所有元素都被插入到已排序部分中。
每一次要找到合适的位置插入。
案例:成绩排序
先将成绩单上第一个同学的名字和成绩写到旁边一张白纸的中央, 如果第二个同学成绩比他高,就写到第一个同学的上方,如果比他低,就写到下方。 看到第三个同学的成绩后,根据他的成绩与前两个同学成绩的比较,插入到相应的位置。比如他的成绩正好在两个同学之间,就在旁边那张排序的纸上,把他的名字插入到前两个人之间。
代码示例: Java 实现插入排序
- i代表需要插入的元素的位置:
1~arr.lenght-1
- j代表前一部分每一个元素的位置:
0~i-1
//该方法接受一个整型数组作为参数,对其进行插入排序。
//在方法中,变量 n 存储数组的长度。
//接着使用一个循环,从数组的第二个元素开始遍历,将其插入到已排序部分中。
//变量 key 存储当前要插入的元素,变量 j 存储已排序部分中最后一个元素的下标。使用一个 while 循环,将已排序部分中大于 key 的元素后移一位,直到找到 key 的插入位置。最后将 key 插入到数组中。
public static void insertionSort(int[] arr) {
int n = arr.length;//存储数组的长度
for (int i = 1; i < n; i ) {//从数组的第二个元素开始遍历,将其插入到已排序部分中
int key = arr[i];// 存储当前要插入的元素
int j = i - 1;//存储已排序部分中最后一个元素的下标
while (j >= 0 && arr[j] > key) {//将已排序部分中大于 key 的元素后移一位,直到找到 key 的插入位置
arr[j 1] = arr[j];
j--;
}
arr[j 1] = key;//将 key 插入到数组中
}
}
4.4 归并排序
归并排序利用了少做事情的思想,对冒泡排序进行改进,时间复杂度为 O(nlogn)
。
思想:将待排序的序列分成若干个子序列,每个子序列都是有序的,然后再将子序列间有序地合并成一个有序序列。
具体实现分为两步:分割序列 合并序列
分割序列:将待排序序列不断分割成两个子序列,直到每个子序列只有一个元素为止。 合并序列:将相邻的两个子序列有序地合并成一个有序序列,直到最终序列有序为止。
代码实现归并排序
代码语言:javascript复制//mergeSort 方法对序列进行分割,merge 方法对分割后的序列进行合并。其中,temp 数组用于存储合并后的序列,i 和 j 分别表示左右子序列的起始位置,k 表示 temp 数组的当前位置。
public class MergeSort {
//对序列进行分割
public static void mergeSort(int[] arr, int left, int right) {
if (left < right) {
int mid = (left right) / 2;
// 分割序列
mergeSort(arr, left, mid);
mergeSort(arr, mid 1, right);
// 合并序列
merge(arr, left, mid, right);
}
}
//对分割后的序列进行合并
public static void merge(int[] arr, int left, int mid, int right) {
int[] temp = new int[right - left 1];//temp 数组用于存储合并后的序列
int i = left, j = mid 1, k = 0;//i 和 j 分别表示左右子序列的起始位置,k 表示 temp 数组的当前位置。
while (i <= mid && j <= right) {
if (arr[i] < arr[j]) {
temp[k ] = arr[i ];
} else {
temp[k ] = arr[j ];
}
}
while (i <= mid) {
temp[k ] = arr[i ];
}
while (j <= right) {
temp[k ] = arr[j ];
}
for (int p = 0; p < temp.length; p ) {
arr[left p] = temp[p];
}
}
public static void main(String[] args) {
int[] arr = {5, 3, 8, 6, 4};
mergeSort(arr, 0, arr.length - 1);
System.out.println(Arrays.toString(arr));
}
}
4.5 快速排序(常用的排序算法)
基于比较的排序算法,其时间复杂度为平均情况下的 O(nlogn),最坏情况下的 O(n^2)。
Java 实现快速排序的代码:
代码语言:javascript复制
sort
是快速排序的主要函数,它调用了quickSort
函数来实现排序。quickSort
函数使用了一个pivot
元素来将数组分为两个子数组,并递归对它们进行排序。 在每一次递归中,首先选择一个pivot
元素,然后从数组两端开始扫描,找到两个元素,一个在左边,一个在右边,它们本来应该在pivot
的左边和右边,但由于位置不正确,需要交换它们。一直重复这个过程,直到最后子数组只有一个元素为止。
public class QuickSort {
public static void sort(int[] arr) {
if (arr == null || arr.length == 0) {
return;
}
quickSort(arr, 0, arr.length - 1);
}
private static void quickSort(int[] arr, int left, int right) {
if (left >= right) {
return;
}
int pivot = arr[(left right) / 2];//用了 `pivot` 元素来将数组分为两个子数组,并递归对它们进行排序
int i = left;
int j = right;
while (i <= j) {
while (arr[i] < pivot) {
i ;
}
while (arr[j] > pivot) {
j--;
}
if (i <= j) {
swap(arr, i, j);
i ;
j--;
}
}
quickSort(arr, left, j);
quickSort(arr, i, right);
}
private static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
4.6 Java 堆排序
思想:基于二叉堆的排序算法,它利用了堆的性质来进行排序。
堆排序分为两个主要步骤:建堆和排序。
- 建堆的过程是将待排序的数组构建成一个二叉堆,通常使用最大堆(大顶堆)来进行排序。
- 排序的过程是不断地从堆顶取出最大值(根节点),将其与堆中最后一个元素交换,然后重新调整堆,使得剩余元素仍满足堆的性质。
//定义了一个 heapSort 方法,它接受一个整数数组作为参数,然后对数组进行堆排序。
//在排序过程中,首先通过 heapify 方法建立一个最大堆。
//然后,从数组末尾开始,将最大值(根节点)与当前位置的元素交换,接着调整堆,使得交换后的剩余元素仍满足堆的性质。
//最终,通过不断重复这个过程,得到一个有序的数组。
public class HeapSort {
public static void heapSort(int[] arr) {
int n = arr.length;
// 建堆(构造最大堆)
for (int i = n / 2 - 1; i >= 0; i--) {
heapify(arr, n, i);
}
// 排序
for (int i = n - 1; i > 0; i--) {
// 将当前最大值(根节点)与最后一个元素交换
int temp = arr[0];
arr[0] = arr[i];
arr[i] = temp;
// 调整堆,使剩余元素仍满足最大堆性质
heapify(arr, i, 0);
}
}
public static void heapify(int[] arr, int n, int i) {
int largest = i; // 初始化最大值为当前节点
int left = 2 * i 1; // 左子节点索引
int right = 2 * i 2; // 右子节点索引
// 如果左子节点大于最大值节点,更新最大值节点
if (left < n && arr[left] > arr[largest]) {
largest = left;
}
// 如果右子节点大于最大值节点,更新最大值节点
if (right < n && arr[right] > arr[largest]) {
largest = right;
}
// 如果最大值节点不是当前节点,交换节点并递归调整堆
if (largest != i) {
int temp = arr[i];
arr[i] = arr[largest];
arr[largest] = temp;
heapify(arr, n, largest);
}
}
public static void main(String[] args) {
int[] arr = {3, 1, 5, 2, 4};
heapSort(arr);
for (int i : arr) {
System.out.print(i " ");
}
}
}