1 认识时间复杂度和简单排序算法

目录

1 认识时间复杂度

1.1 常数时间的操作

1.2 额外空间复杂度

2 简单排序算法

2.1 选择排序

2.2 冒泡排序

补充:异或运算

 异或运算应用

2.3 插入排序

3  二分法的详解与扩展

3.1在一个有序数组中,找某个数是否存在

3.2在一个有序数组中,找>=某个数最左侧的位置

3.3 局部最小值问题

4 对数器的概念和使用

5 剖析递归行为和递归行为时间复杂度的估算


1 认识时间复杂度

1.1 常数时间的操作

一个操作如果和样本的数据量没有关系,每次都是固定时间内完成的操作,叫做常数操作。

时间复杂度为一个算法流程中,常数操作数量的一个指标。常用O(读作big O)来表示。具体来说,先要对一个算法流程非常熟悉,然后在去写出这个算法流程中,发生了多少的常数操作,进而总结出常数操作数量的表达式。

在表达式中,只要高阶项,不要低阶项,也不要高阶项的系数,剩下的部分如果为f(N),那么时间复杂度为O(f(N))。

评价一个算法流程的好坏,先看时间复杂度的指标,然后再分析不同数据样本下的实际运行时间,也就是“常数项时间”。(进行常数操作的时间可能是不一样的)

例如在下面的这段代码中process1和process2这两个方法的时间复杂度都是O(N),然后比较常数项,最后比较实际的运行时间,才能知道哪个算法更优。

public class Test {

    public static void process1() {
        int N = 1000;
        int a = 0;
        for (int i = 0; i < N; i++) {
            a = 2 + 5;
            a = 4 * 7;
            a = 6 * 8;
        }
    }

    public static void process2() {
        int N = 1000;
        int a = 0;
        for (int i = 0; i < N; i++) {
            a = 3 | 6;
            a = 3 & 4;
            a = 4 | 785;
        }
    }

    public static void main(String[] args) {
        process1();
        process2();
    }

}

1.2 额外空间复杂度

只需要有限的变量就可以完成算法流程的话,额外空间复杂度就是O(1)

如果必须开辟一个额外的数组,这数组还和原来的数组等规模,额外空间复杂度就是O(N)

2 简单排序算法

2.1 选择排序

  • 看(从剩下的数中找出最小的数)的次数:N + N - 1 + N - 2 + … + 1
  • 比较(每次看的数,和既定数作比较):N + N - 1 + N - 2 + … + 1
  • 交换swap: N 次

不难看出前两次操作的次数的和可以用等差数列求和来求。

等差数列求和公式 : S_{n}=n*a_{1}+n(n-1)d/2  或S_{n}=n(a_{1}+a_{n})/2

所以进行的总操作数可以粗略的写为:aN^{2}+bN+c

所以说,选择排序是一个时间复杂度为 O(N^{2}) 的算法。

代码实现如下:

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);
   }
}

public static void swap(int[] arr, int i, int j) {
   int tmp = arr[i];
   arr[i] = arr[j];
   arr[j] = tmp;
}

2.2 冒泡排序

  • 比较两个相邻的元素,如果第一个数比第二个数大,就交换他们两个。
  • 从开始到结尾的每一对,对每一对相邻元素做相同的操作。因此,最后的元素会是最大的数。
  • 针对所有的元素重复以上的步骤,除了上次操作的最后一个数。(因为最后一个已经排好,是最大的数)

不难看出最后进行的总操作数仍然为一个等差数列求和,故冒泡排序也是一个时间复杂度为O(N^{2}) 的算法。

代码实现:

public static void bubbleSort(int[] arr) {
   if (arr == null || arr.length < 2) {
      return;
   }
   for (int e = arr.length - 1; e > 0; e--) {  // 0 ~ N-1上做一轮;下回就是 0 ~ N - 2
      for (int i = 0; i < e; i++) {
         if (arr[i] > arr[i + 1]) {
            swap(arr, i, i + 1);
         }
      }
   }
}

// 交换arr的i和j位置上的值,前提i ≠ j
public static void swap(int[] arr, int i, int j) {
   arr[i] = arr[i] ^ arr[j];
   arr[j] = arr[i] ^ arr[j];
   arr[i] = arr[i] ^ arr[j];
}
补充:异或运算

相同为0,不同为1。也可以理解为无进位相加。eg:10010+01110=11100

性质:

  • 0 ^ N = N 、N ^ N = 0
  • 满足交换律和结合律

根据上面这两条性质可以推出:a ^ b ^ c ^ d ^ e ^ f .... m ^ n 它的运算结果跟abcdef进行异或运算的顺序无关。

利于这些性质可以使用异或操作来进行数值交换,例如交换a和b的值,可用下面代码实现

int a = c; //c代表一个数
int b = d; //d代表一个数

a = a ^ b; //这行代码跑完,a = c ^ d, b = d
b = a ^ b; //这行代码跑完,a = c ^ d, b = c ^ d ^ d = c
a = a ^ b; //这行代码跑完,a = c ^ d ^ c = d, b = c  交换完成

使用这种方式交换数值可以不用额外的变量,即不用申请额外的空间。

但是注意能够使用这个方法的前提是,要交换数值的两个变量在内存中是两个独立的区域,即a和b 指向内存中不同的空间,否则结果一直为0。(a和b的数值可以相同,数值存放的内存空间不能相同)

尽量别使用这个方法

 异或运算应用

面试题:在一个整型数组中,已知只有一种数出现了奇数次,其他所有数都出现了偶数次,怎么找出出现奇数次的数?如果有两种数出现了奇数次,其他所有数都出现了偶数次,怎么找出这两种奇数次的数?要求时间复杂度为O(N),额外空间复杂度为O (1)。

解:①

public static void printOddTimesNum1(int[] arr) {
   int eor = 0;
   for (int cur : arr) {
      eor ^= cur;
   }
   System.out.println(eor);
}

因为异或运算和运算顺序无关,得到的结果为一个。所以可以先让出现偶数次的数先异或结果为0,剩下奇数次的数再异或得到它自己。

public static void printOddTimesNum2(int[] arr) {
    int eor = 0;

    // 得到eor = a ^ b
    for (int i = 0; i < arr.length; i++) {
        eor ^= arr[i];
    }
    // eor 必有一个位置上是1

    // 提取出eor最右侧的1(位运算的常见操作)& 两个位都为1时,结果才为1,~ 0变1,1变0 
    int rightOne = eor & (~eor + 1);  
    int onlyOne = 0;  //eor'

    for (int cur : arr) { //cur为arr中的一个数
        if ((cur & rightOne) == 1) { //cur中与eor最右侧的1位数相同的位也为1
            onlyOne ^= cur;
        }
    }
    System.out.println(onlyOne + " " + (eor ^ onlyOne));
}

假设a和b这两个数是出现奇数次的两个数

同样设变量 eor,在数组arr 中从头到尾进行异或,最后 eor = a ^ b,且 已知a ≠ b,所以 eor ≠ 0

那么a 和 b 的某一位上一定不一样(不然 a ^ b 就等于0 了)

即 eor 一定有一位是1

假设eor 的第n位为1,(int整数32位),根据这个第n位把数组中的数进行分类

  • 第一类为第n位 = 1的数
  • 第二类为第n位 = 0的数

所以,a和b一定分别属于两边。现在再来一个变量eor',eor' 只去异或第n位上为1的那些数,这样 eor’ 到最后一定是a 或者 b

即eor’ = a or b

现在另一个数只需要用 eor ^ eor'即可求出

位运算比算术运算快多了

2.3 插入排序

 插入排序算法的一般步骤:

  1.  从第一个元素开始,该元素可以认为已被排序;
  2.  取出下一个元素,在已经排序的元素序列中从后向前扫描;
  3.  如果该元素(已排序)大于新元素,将该元素移到下一个位置;
  4.  重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
  5.  将新元素插入到该位置后,重复2~5

插入排序与数据状况有关系,影响排序流程进行。

所以最差时间复杂度:O(n^2),最优时间复杂度:O(n),平均时间复杂度:O(n^2)

代码实现如下 :

public static void insertionSort(int[] arr) {
   if (arr == null || arr.length < 2) {
      return;
   }
    
   //0 ~ 0 有序的
   //0 ~ i 想有序
   for (int i = 1; i < arr.length; i++) { // 从索引为1 开始看,0上的数不用看
      for (int j = i - 1; j >= 0 && arr[j] > arr[j + 1]; j--) { // 不越界为前提
         swap(arr, j, j + 1);
      }
   }
}

// 交换arr的i和j位置上的值,前提i ≠ j
public static void swap(int[] arr, int i, int j) {
   arr[i] = arr[i] ^ arr[j];
   arr[j] = arr[i] ^ arr[j];
   arr[i] = arr[i] ^ arr[j];
}

3  二分法的详解与扩展

3.1在一个有序数组中,找某个数是否存在

基本思想是:目标值通过与中间元素比较,可分为三种情况。第一种情况:目标值与中间元素相等,查找结束;第二种情况:目标值比中间元素大,则把后半部分的中间元素与目标值比较;第二种情况:目标值比中间元素小,则把前半部分的中间元素与目标值比较;这三步一直循环,直到查找结束。

所以时间复杂度为O( \log _{2}N )

代码实现如下:

public static boolean exist(int[] sortedArr, int num) {
   if (sortedArr == null || sortedArr.length == 0) {
      return false;
   }
   int L = 0;
   int R = sortedArr.length - 1;
   int mid = 0;
   while (L < R) {
      mid = L + ((R - L) >> 1);
      if (sortedArr[mid] == num) {
         return true;
      } else if (sortedArr[mid] > num) {
         R = mid - 1;
      } else {
         L = mid + 1;
      }
   }
   return sortedArr[L] == num;
}
3.2在一个有序数组中,找>=某个数最左侧的位置

时间复杂度也为O( \log _{2}N )

与3.1的区别在于3.1找到数就可以返回,3.2是二分到一个范围上没有数才返回。

代码实现如下:

// 在arr上,找满足>=value的最左位置
public static int nearestIndex(int[] arr, int value) {
   int L = 0;
   int R = arr.length - 1;
   int index = -1;
   while (L < R) {
      int mid = L + ((R - L) >> 1);
      if (arr[mid] >= value) {
         index = mid;
         R = mid - 1;
      } else {
         L = mid + 1;
      }
   }
   return index;
}
3.3 局部最小值问题

局部最小:对于0 位置和 1位置上的数,如果 0位置上的数 < 1位置上的数 那么0位置上的数就是局部最小,N-1位置上的数 和 N位置上的数,如果N-1 位置上的数> N位置上的数那么N 位置上的数的就是局部最小,对于 i-1、i、 i+1 三个位置上的数,有i - 1位置上的数 > i位置上的数 且 i位置上的数 < i + 1 位置上的数),那么 i 位置上的数就是局部最小。


arr 数组无序,任何两个相邻的数一定不相等,求其局部最小值。(不一定有序才能二分)

要求:时间复杂度好于 O(N)

代码实现如下:

public static int getLessIndex(int[] arr) {
   if (arr == null || arr.length == 0) {
      return -1; // no exist
   }
   if (arr.length == 1 || arr[0] < arr[1]) {
      return 0;
   }
   if (arr[arr.length - 1] < arr[arr.length - 2]) {
      return arr.length - 1;
   }
   int left = 1;
   int right = arr.length - 2;
   int mid = 0;
   while (left < right) {
      mid = (left + right) / 2;
      if (arr[mid] > arr[mid - 1]) {
         right = mid - 1;
      } else if (arr[mid] > arr[mid + 1]) {
         left = mid + 1;
      } else {
         return mid;
      }
   }
   return left;
}

4 对数器的概念和使用

  1. 有一个你想要测的方法a
  2. 实现复杂度不好但是容易实现的方法b
  3. 实现一个随机样本产生器
  4. 把方法a和方法b跑相同的随机样本,看看得到的结果是否一样。
  5. 如果有一个随机样本使得比对结果不一致,打印样本进行人工干预,改对方法a或者方法b
  6. 当样本数量很多时比对测试依然正确,可以确定方法a已经正确。

比如我们之前写的选择排序 ,自己实现的方法作为方法a,也是我们想测的方法

方法b:系统函数排序

// for test
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;
}

完整代码

// for test
public static void comparator(int[] arr) {
	Arrays.sort(arr);
}

// for test
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;
}

// for test
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;
}

// for test
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;
}
// for test
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();
}
// for test
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);
		selectionSort(arr1);
		comparator(arr2);
		if (!isEqual(arr1, arr2)) {
			succeed = false;
			printArray(arr1);
			printArray(arr2);
			break;
		}
	}
	System.out.println(succeed ? "Nice!" : "Fucking fucked!");

	int[] arr = generateRandomArray(maxSize, maxValue);
	printArray(arr);
	selectionSort(arr);
	printArray(arr);
}

5 剖析递归行为和递归行为时间复杂度的估算

用递归方法找一个数组中的最大值,系统上到底是怎么做的?

master公式的使用

T(N) = a*T(N/b) + O(N^{d})

  • T(N):母问题数据量为N

  • T(N/b):调用的子过程每次数据量等量,都是 N/b(子问题规模)

  • a:子问题等量情况下,被调了多少次

  • O(N^{d}):除去调用子过程,剩下操作的时间复杂度

1)log_{b}a < d  =>时间复杂度为O( N^{d})

2) log_{b}a > d =>时间复杂度为O( N^{log_{b}a} )

3) log_{b}a = d =>时间复杂度为O( N^{d}*logN )

代码实现如下:

public static int getMax(int[] arr) {
   return process(arr, 0, arr.length - 1);
}

public static int process(int[] arr, int L, int R) {
   if (L == R) {
      return arr[L];
   }
   
   int mid = L + ((R - L) >> 1);  // 防止溢出 位运算要比算术运算快
   int leftMax = process(arr, L, mid);
   int rightMax = process(arr, mid + 1, R);
   return Math.max(leftMax, rightMax);
}

根据master公式不难算出上述代码的时间复杂度为O(N)

  • 29
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Diligent programmer

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值