线性查找法02:循环不变量和算法复杂度
循环不变量
public static<E> int search(E[] arr, E target){
/**
* 循环不变量,就是在循环中始终遵守的原则
* 因为在arr[0...i-1]中没有找到目标,所以才继续循环
* 这个arr[0...i-1]就是循环不变量,在写循环时一定要清楚循环不变量是什么
*/
for (int i = 0; i < arr.length; i++) {
/**
* 循环体就是为了维持循环不变量,证明算法的正确性
*/
if (target == arr[i]) {
return i;
}
}
return -1;
}
算法复杂度
n表示数据规模,O(f(n))表示运行算法所需要执行的指令数,和f(n)成正比
复杂度描述的是随着数据规模n的增大,算法性能的变化趋势,在复杂度分析中,常数不重要一般忽略
线性查找法的复杂度是O(n)
T1 | 10000n | O(n) |
---|---|---|
T2 | 2n^2 | O(n^2) |
当n > 5000,T2复杂度大于T1,且n越大,差距越大 |
常见的时间复杂度
/**
* 遍历一个n*n的二维数组,n指维度,复杂度是O(n^2)
*/
for (int i = 0; i < n; i++){
for (int j = 0; j < n; j++){
return arr[i][j];
}
}
/**
* 遍历一个a*a的二维数组,其中a*a=n,n指元素总数,复杂度是O(n)
* 因此一定要清楚n到底指的是谁
*/
for (int i = 0; i < a; i++){
for (int j = 0; j < a; j++){
System.out.println(arr[i][j]);
}
}
/**
* 输出数字num的二进制位数,n和要计算的次数有关,每计算一次,规模减半,复杂度是O(logn)
* 对log函数而言,底数不同相差的是常数,因此忽略底数
*/
while (num){
System.out.println(num % 2);
num /= 2;
}
/**
* 输出数字num的约数,约数是成对出现的,只要循环到根号num就可以拿到所有约数,因此n指根号num
* 复杂度是O(sqrt{n})
*/
for (int i = 1; i * i <= n; i++) {
if (n % i == 0){
System.out.println(i);
System.out.println(n / i);
}
}
/**
* 输出长度为n的二进制数字,每增加一个长度,规模翻倍,复杂度是O(2^n)
*/
/**
* 输出长度为n的数组的全排列,复杂度是O(n!)
*/
/**
* 判断数字n是否是偶数,只用执行常数量级的语句,复杂度是O(1)
*/
总结:O(1) < O(logn) < O(sqrt(n)) < O(n) < O(nlogn) < O(n^2) < O(2^n) < O(n!)
一个时间复杂度的问题
有一个字符串数组,将数组中的每一个字符串按照字母序排序;之后再将整个字符串数组按照字典序排序,这个过程的时间复杂度是多少?
每个字符串的长度和字符串数组的长度没有联系,需要分开计算
假设最长的字符串长度为s,数组的长度为n
则对每个字符串排序的复杂度为O(slogs),总共为O(nslogs)
而对整个字符串数组排序时,比较两个字符串的顺序,复杂度为O(s)而不是O(1),总共为O(snlogn)
故整个过程的时间复杂度为O(nslogns)
特殊用例下的时间复杂度
插入排序法时间复杂度为O(n^2),快速排序法时间复杂度为O(nlogn)
但是当测试用例完全有序时,插入排序法会升为O(n),而快速排序法会降为O(n^2)
数据规模的概念
对于一个O(n)级别的算法,在不同数据规模下运行耗费的时间,如果想要在1s之内完成运算,O(n)算法的规模大概为10^8
O(nlogn)算法的规模大概为10^7
而O(n^2)算法的规模只能有10000
因此可以根据数据规模的要求,选择设计复杂度不同的算法
数据规模 | 消耗时间 |
---|---|
10^3 | 2.5e-06 s |
10^4 | 2.5e-05 s |
10^5 | 0.00025 s |
10^6 | 0.0025 s |
10^7 | 0.025 s |
10^8 | 0.25 s |
10^9 | 2.5 s |
public class Algorithm {
public static void main(String[] args) {
long start = System.nanoTime();
int sum = 0;
for (int i = 0; i < 1000000000; i++) {
sum += i;
}
long end = System.nanoTime();
System.out.println((end - start) / 1000000000.0 + "s");
}
}
测试线性查找算法性能
public class Algorithm {
public static void main(String[] args) {
Integer n = 10000000;
Integer[] arr = ArrayGenerator.generatorArray(n);
Integer target = n;
/**
* System.nanoTime()方法打印时间戳
*/
long startTime = System.nanoTime();
/**
* 循环多次测试时间性能,比一次性测试更稳定,结果也更真实
*/
for (int i = 0; i < 100; i++) {
LinerSearch.search(arr, target);
}
long endTime = System.nanoTime();
System.out.println((endTime - startTime) / 1000000000.0 + "秒");
}
}
class LinerSearch {
private LinerSearch(){}
public static<E> int search(E[] arr, E target){
for (int i = 0; i < arr.length; i++) {
if (arr[i].equals(target)) {
return i;
}
}
return -1;
}
}
/**
* 创建一个根据n生成数组的类
*/
class ArrayGenerator {
private ArrayGenerator(){}
public static Integer[] generatorArray(Integer n){
Integer[] arr = new Integer[n];
for (int i = 0; i < n; i++) {
arr[i] = i;
}
return arr;
}
}