我们经常说算法,哪个算法好,哪个算法差,算法阿尔法狗战胜了人类围棋之类的话题,那到底,什么是算法?
算法,就是能够对一定规范的输入,在有限时间内获得所要求的输出。一个算法的优劣可以用空间复杂度与时间复杂度来衡量。
那我们从时间复杂度入手,开始学习:
1.时间复杂度
认识时间复杂度
常数时间的操作:一个操作如果和数据量没有关系,每次都是 固定时间内完成的操作,叫做常数操作。
时间复杂度为一个算法流程中,常数操作数量的指标。常用O (读作big O)来表示。具体来说,在常数操作数量的表达式中,
只要高阶项,不要低阶项,也不要高阶项的系数,剩下的部分 如果记为f(N),那么时间复杂度为O(f(N))。
评价一个算法流程的好坏,先看时间复杂度的指标,然后再分 析不同数据样本下的实际运行时间,也就是常数项时间。
上面就是时间复杂度的讲解,以及大O 表示法,有些抽象,下面搭配一个实例,来具体讲解:
例子一
一个简单的理解时间复杂度的例子
一个有序数组A,另一个无序数组B,请打印B中的所有不在A中的数,A数
组长度为N,B数组长度为M。
算法流程1:对于数组B中的每一个数,都在A中通过遍历的方式找一下;
算法流程2:对于数组B中的每一个数,都在A中通过二分的方式找一下;
算法流程3:先把数组B排序,然后用类似外排的方式打印所有在A中出现的数;
三个流程,三种时间复杂度的表达…
如何分析好坏?
首先,我们要一个个分析他们的时间复杂度:
算法流程一: 对于数组B中的每一个数,都在A中通过遍历的方式找一下;
这种方法是最浅显易懂的,相信也是大家看到首先想到的方法。很好理解,从数组B里面拿出第一个数和数组A里面所有数字比较,再拿出第二个和数组A比较,以此往复,直到数组B所以数字都比过为止。相当于两个for循环,时间复杂度为O(m*n)
算法流程2:对于数组B中的每一个数,都在A中通过二分的方式找一下;
讲解这个时间复杂度之前呢,我想先说明一个二分法,也许觉得多余,但这很重要。
二分法
假设有一个有序数组 0,1,3,4,6,7,8,56
现在呢,我们要找这个数组中是否有一个数字,怎么查找?
全部遍历一遍?当然可以。但是这样做的话,假设有n个元素,全部便利一遍,时间复杂度即为O(n),太高,那怎么样才可以让复杂度低一些呢?二分法,也许是一个好的选择。
我们把这个有序数组第一个元素坐标设置为L,最后一个元素坐标,设置为R,我们可以通过,(L+R)/2 来获取到数组中间元素,记为middle,判断middle和要找的数大小关系,如果大,就使用middle与R取中间的值,作比较,循环往复,找到为止,反之亦然。
一次切一半,极大减少了我们的查找时间,降低了时间复杂度。时间复杂度为O(log2(N))
回到题目,数组B中的每一个数,都在A中通过二分的方式找一下,很明显,
每次查找的时间复杂度,即为二分法的时间复杂度,为O(log2(N)),由于数组B中,每一个都要查找,即最终时间复杂度为O(M*log2(N))
算法流程3:先把数组B排序,然后用类似外排的方式打印所有在A中出现的数;
第三中方法,两步进行,第一步,排序;第二步,类似外排方式,按照最差情况计算
首先看第一步,排序,数组B使用二分排序,时间复杂度为O(M*log2(M))
再看第二步,数组B排序完成之后,数组A和B就都是有序的了,我们假设一个a指针,指向数组A第一个元素,再假设一个数组b指向数组B第一个元素,然后两个指针想对应的数字进行比较,然后根据比较大小,进行指针之间的移动。
a指针移动的条件是,a指针指向的数字<b指针指向的数字时,向右移动一位。
b指针移动的条件是,b指针指向的数字<=a指针指针指向的数字,如果=只向右移动,不打印,如果<,打印数字,且移动
下面配图,图比较丑,不要教真:
初始状态:
由于指针a所对应的数字1小于指针b所对应的数字2,故b指针不动,a指针向右移动一位.
继续比较,现在a指针所对应的数字3大于b指针所对应的数字2,故打印出2数字,并b指针向右移动一位.
此时,a指针所指针的数字和b指针所指的数字一致,故b指针向右移动一位。
循环往复以上步骤即可,当指针a和指针b任意一个指针走到尽头,就意味着程序的中止,我们假设最坏情况,同时到最后才结束,那么他的时间复杂度为0(N+M)
再加上之前排序的时间复杂度,最终复杂度为O(M*log2(M))+O(N+M)
现在,我们回到题目,比较三个方法的时间复杂度
方法一:O(mn)
方法二:O(Mlog2(N))
方法三:O(M*log2(M))+O(N+M)
一眼,我们就能排除方法一,它无疑,是复杂度最高的。我们来看二和三。二和三其实在没有具体数据之前,我们并没有办法分辨,哪一个算好更好,如果A数组很短,B很长,二更好;相反情况下,三更好
2.冒泡排序
冒泡排序,是一种比较简单的排序算法。
冒泡原理如下:
1.比较相邻的元素。如果第一个比第二个大,就交换他们两个。
2.对每一对相邻元素做同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。
3.针对所有的元素重复以上的步骤,除了最后一个。
4.持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
冒泡排序java代码:
public class Code_00_BubbleSort {
public static void bubbleSort(int[] arr){
if(arr==null||arr.length<2){
return;
}
for(int end=arr.length-1;end>0;end--){
for (int i=0;i<end;i++){
if(arr[i]>arr[i+1]){
swap(arr,i,i+1);
}
}
}
}
private static void swap(int[] arr,int i,int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
}
时间复杂度O(N^2),额外空间复杂度O(1)
3.选择排序
选择排序是一种简单直观的排序算法。
选择排序原理如下:
n个记录的文件的直接选择排序可经过n-1趟直接选择排序得到有序结果:
①初始状态:无序区为R[1…n],有序区为空。
②第1趟排序
在无序区R[1…n]中选出关键字最小的记录R[k],将它与无序区的第1个记录R[1]交换,使R[1…1]和R[2…n]分别变为记录个数增加1个的新有序区和记录个数减少1个的新无序区。
……
③第i趟排序
第i趟排序开始时,当前有序区和无序区分别为R[1…i-1]和R(i…n)。该趟排序从当前无序区中选出关键字最小的记录 R[k],将它与无序区的第1个记录R交换,使R[1…i]和R分别变为记录个数增加1个的新有序区和记录个数减少1个的新无序区。
选择排序java代码:
public class Code_01_SelectionSort {
public static void selectionSort(int[] arr){
if(arr==null||arr.length<2){
return;
}
for(int i=0;i<arr.length-1;i++){
int minIndex = i;
for(int j=i+1;j<arr.length;j++){
minIndex = arr[j]<arr[minIndex]?j:minIndex;
}
swap(arr,i,minIndex);
}
}
private static void swap(int[] arr,int i,int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
}
时间复杂度O(N^2),额外空间复杂度O(1)
4.插入排序
插入排序,一般也被称为直接插入排序。对于少量元素的排序,它是一个有效的算法 。
插入排序原理:
插入排序的工作方式像许多人排序一手扑克牌。开始时,我们的左手为空并且桌子上的牌面向下。然后,我们每次从桌子上拿走一张牌并将它插入左手中正确的位置。为了找到一张牌的正确位置,我们从右到左将它与已在手中的每张牌进行比较。拿在左手上的牌总是排序好的,原来这些牌是桌子上牌堆中顶部的牌 。
插入排序是指在待排序的元素中,假设前面n-1(其中n>=2)个数已经是排好顺序的,现将第n个数插到前面已经排好的序列中,然后找到合适自己的位置,使得插入第n个数的这个序列也是排好顺序的。按照此法对所有元素进行插入,直到整个序列排为有序的过程,称为插入排序 。
插入排序图解:
java代码实现:
public class Code_02_InsertionSort {
public static void insertSort(int arr[]){
if(arr==null||arr.length<2){
return;
}
for(int i=1;i<arr.length;i++){
for(int j=i-1;j>0&&arr[j]>arr[j+1];j--){
swap(arr,j,j+1);
}
}
}
private static void swap(int[] arr,int i,int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
}
最差时间复杂度O(N^2),最好时间复杂度O(N)额外空间复杂度O(1)
5.对数器
在很多情况下,我们写了一个算法,那我们要怎么去验证这个算法对不对呢?这就需要需要用到我们的计数器
对数器的概念和使用
0,有一个你想要测的方法a,
1,实现一个绝对正确但是复杂度不好的方法b,
2,实现一个随机样本产生器
3,实现比对的方法
4,把方法a和方法b比对很多次来验证方法a是否正确。
5,如果有一个样本使得比对出错,打印样本分析是哪个方法出 错
6,当样本数量很多时比对测试依然正确,可以确定方法a已经 正确。
java代码实现(以检测冒泡排序为例):
0,有一个你想要测的方法a
public class Code_00_BubbleSort {
public static void bubbleSort(int[] arr){
if(arr==null||arr.length<2){
return;
}
for(int end=arr.length-1;end>0;end--){
for (int i=0;i<end;i++){
if(arr[i]>arr[i+1]){
swap(arr,i,i+1);
}
}
}
}
private static void swap(int[] arr,int i,int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
}
1,实现一个绝对正确但是复杂度不好的方法b,
public static void comparator(int[] arr) {
Arrays.sort(arr);
}
2,实现一个随机样本产生器
public static int[] generateRandomArray(int maxSize, int maxValue) {
int[] arr = new int[(int) ((maxSize + 1) * Math.random())];
for (int i = 0; i < arr.length; i++) {
arr[i] = (int) ((maxValue + 1) * Math.random()) - (int) (maxValue * Math.random());
}
return arr;
}
3,实现比对的方法
public static boolean isEqual(int[] arr1, int[] arr2) {
if ((arr1 == null && arr2 != null) || (arr1 != null && arr2 == null)) {
return false;
}
if (arr1 == null && arr2 == null) {
return true;
}
if (arr1.length != arr2.length) {
return false;
}
for (int i = 0; i < arr1.length; i++) {
if (arr1[i] != arr2[i]) {
return false;
}
}
return true;
}
4,把方法a和方法b比对很多次来验证方法a是否正确。
5,如果有一个样本使得比对出错,打印样本分析是哪个方法出 错
6,当样本数量很多时比对测试依然正确,可以确定方法a已经 正确。
public static void main(String[] args) {
int testTime = 500000;
int maxSize = 100;
int maxValue = 100;
boolean succeed = true;
for (int i = 0; i < testTime; i++) {
int[] arr1 = generateRandomArray(maxSize, maxValue);
int[] arr2 = copyArray(arr1);
bubbleSort(arr1);
comparator(arr2);
if (!isEqual(arr1, arr2)) {
succeed = false;
break;
}
}
System.out.println(succeed ? "Nice!" : "Fucking fucked!");
int[] arr = generateRandomArray(maxSize, maxValue);
printArray(arr);
bubbleSort(arr);
printArray(arr);
}
完工。输出结果:
附上完整代码:
public class Code_00_BubbleSort {
//冒泡排序
public static void bubbleSort(int[] arr){
if(arr==null||arr.length<2){
return;
}
for(int end=arr.length-1;end>0;end--){
for (int i=0;i<end;i++){
if(arr[i]>arr[i+1]){
swap(arr,i,i+1);
}
}
}
}
//俩数字交换位置
private static void swap(int[] arr,int i,int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
// 绝对正确的排序
public static void comparator(int[] arr) {
Arrays.sort(arr);
}
//随机样本产生器
public static int[] generateRandomArray(int maxSize, int maxValue) {
int[] arr = new int[(int) ((maxSize + 1) * Math.random())];
for (int i = 0; i < arr.length; i++) {
arr[i] = (int) ((maxValue + 1) * Math.random()) - (int) (maxValue * Math.random());
}
return arr;
}
// 对比的方法
public static boolean isEqual(int[] arr1, int[] arr2) {
if ((arr1 == null && arr2 != null) || (arr1 != null && arr2 == null)) {
return false;
}
if (arr1 == null && arr2 == null) {
return true;
}
if (arr1.length != arr2.length) {
return false;
}
for (int i = 0; i < arr1.length; i++) {
if (arr1[i] != arr2[i]) {
return false;
}
}
return true;
}
public static int[] copyArray(int[] arr) {
if (arr == null) {
return null;
}
int[] res = new int[arr.length];
for (int i = 0; i < arr.length; i++) {
res[i] = arr[i];
}
return res;
}
public static void printArray(int[] arr) {
if (arr == null) {
return;
}
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + " ");
}
System.out.println();
}
public static void main(String[] args) {
int testTime = 500000;
int maxSize = 100;
int maxValue = 100;
boolean succeed = true;
for (int i = 0; i < testTime; i++) {
int[] arr1 = generateRandomArray(maxSize, maxValue);
int[] arr2 = copyArray(arr1);
bubbleSort(arr1);
comparator(arr2);
if (!isEqual(arr1, arr2)) {
succeed = false;
break;
}
}
System.out.println(succeed ? "Nice!" : "Fucking fucked!");
int[] arr = generateRandomArray(maxSize, maxValue);
printArray(arr);
bubbleSort(arr);
printArray(arr);
}
}