常数时间的操作
如果一个操作与数据量的规模无关,其操作的时间是一定的,称其为常数时间的操作。
时间复杂度
时间复杂度是衡量一个算法中发生了多少常数时间操作的指标,通常用O来表示。若一个算法中的常数项操作的数量为kn^m+pn^2+L,则该算法的时间复杂度为O(n^m),删去其常数项、系数与指数次数不是最高的项。
如果两个算法的时间复杂度相同,还想继续比较两个算法的性能,可以生成大量实验数据直接测验实际运行时间,也就是常数项运行时间。
额外的空间复杂度
除了必要的内存空间(存储输入数据的空间与存储输出数据的空间)外,为了实现算法所花费的空间。
选择排序、冒泡排序与复杂度分析
选择排序
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;
}
冒泡排序
public static void bubbleSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
for (int e = arr.length - 1; e > 0; e--) {
for (int i = 0; i < e; i++) {
if (arr[i] > arr[i + 1]) {
swap(arr, i, i + 1);
}
}
}
}
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];
}
选择排序的时间复杂度的计算过程如下
n - 1 + n - 2 + n - 3 + ...... + 1 = ((1 + n - 1 ) * ( n - 1)) / 2 , 时间复杂度表示为O(n ^2) ,额外的空间复杂度是常数个,表示为O(1)。
冒泡排序的时间复杂度的计算过程如下
n - 1 + n - 2 + n - 3 + ...... + 1 = (( n - 1 + 1) * ( n - 1)) / 2,时间复杂度表示为O(n ^2) ,额外的空间复杂度是常数个,表示为O(1)。
插入排序与复杂度分析
实现
class Sort{
public:
static int* insertSort(int* arr,int length){
int i = 1;
int j = 0;
for( ; i < length; i++){
for(j = i - 1; j >= 0; j--){
if(arr[j + 1] > arr[j]){
swap(arr[j + 1],arr[j]);//swap()函数是c++自带的函数,它的工作原理是交换两个指针的值。
}
}
}
return (int*)arr;
}
};
只要数据的数量一定,冒泡排序与选择排序的执行次数就是一定的,而插入排序却不是这样。除了与数据的数量有关外,插入排序的执行次数还与数据的质量有关。如果要求我们返回的是一个按照从小到大的顺序排列的数组,而所给的数据也是从小到大排列的,那么常数时间的操作的数量就是n次;若所给的数据是从大到小给出的,那么常数时间的操作的数量就是((1 + n - 1 ) * ( n - 1)) / 2,与冒泡和选择排序的常数时间的操作的数量相同。
在这种情况下,即常数时间的执行次数与数据的质量有关的情况下,一般按照最坏的情况计算时间复杂度。所以插入排序的时间复杂度为O(n^2).
二分法
现在有3道例题:
1. 在一个有序数组中,查找某数是否存在;
2. 在一个有序数组中,找 >=某个数最左侧的位置;
3. 局部最小值问题(整个数组无序,任意相邻的两个数不相等)。
以上三个问题都可以用二分法来求解。
class BS{
public:
//1. 在一个有序数组中,查找某数是否存在;
static bool isBSExit(int* arr,int length,int num){
int left = 0;
int right = length - 1;
int mid = 0;
for( ; right > left; ){
//cout << left << " " << right << endl;
mid = left + ((right - left) >> 1);
if(arr[mid] > num){
right = mid - 1;
}
else if(arr[mid] < num){
left = mid + 1;
}
else{
return true;
}
}
return arr[right] == num;
}
//2. 在一个有序数组中,找 >= 某个数最左侧的位置;
static int getBSLocation(int* arr,int length,int num){
int left = 0;
int right = length - 1;
int mid = 0;
int location = -1;
for( ; right > left; ){
mid = left + ((right - left) >> 1);
if(arr[mid] > num){
right = mid - 1;
location = mid;
}
else if(arr[mid] < num){
left = mid + 1;
}
}
return location;
}
//3. 局部最小值问题。
static int getMinValue(int* arr, int length){
if(arr == NULL || length == 0)
return -1;
if( arr[0] < arr[1])
return 0;
if (arr[length - 1] < arr[length - 2])
return length - 1;
int left = 1;
int right = length - 2;
int mid = 0;
for( ; left < right; ){
mid = left + ((right - left ) >> 1);
if(arr[mid] < arr[mid + 1] && arr[mid] < arr[mid - 1]){
return mid;
}
else if(arr[mid] > arr[mid - 1]){
right = mid - 1;
}
else if(arr[mid] > arr[mid + 1]){
left = mid + 1;
}
}
}
};
异或
在上面实现冒泡排序的算法中,swap()函数采用了异或的算法来实现位置交换,不用使用额外的变量(但其实此方法并不推荐使用,因为如果传入的两个参数指向的是同一块内存,会导致两个数都变成0)。
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];
}
异或(^) 可以看作是没有进位的加法运算,异或的性质有:a^a = 0; a^0 = a;异或满足交换律与结合律。
所以swap()函数的实现与证明过程如下:
a = a^b;//a = a^b b = b
b = a^b;//a = a^b b = a^b^b = a
a = a^b;//a = a^b^a = b b = a
现在有两道例题:
1. 一个数组中有一种数出现了奇数次,其余出现了偶数次,找出这个数
2. 一个数组中有两种数出现了奇数次,其余出现了偶数次,找出这个两种数
class OddTimes{
public:
//1. 一个数组中有一种数出现了奇数次,其余出现了偶数次,找出这个数
static int findOddTimesNum(int* arr,int length){
int i = 1;
int eor = arr[0];
for( ; i < length; i++){
eor ^= arr[i];
}
return eor;
}
// 2. 一个数组中有两种数出现了奇数次,其余出现了偶数次,找出这个两种数
static void findOddTimesNum_2(int* arr, int length){
int i = 1;
int eor = arr[0];
int rightOne = 0;
for( ; i < length; i++){
eor ^= arr[i];
}
cout <<"eor:" << eor <<endl;
int res_1 = 0,res_2 = 0;
//两种数出现了奇数次,它们的和一定不为0,即一定有至少一位为1,此处rightOne得到的是eor最右边的1的位置位置是1,其余位置是0
rightOne = eor & (~eor + 1);
cout << "rightOne:" << rightOne << endl;
for(i = 0; i < length; i++){
if( (rightOne & arr[i]) != 0){//注意这里 != 的优先级要高于 &
res_1 ^= arr[i];
cout << "i: " << i << " res: "<< res_1 << endl;
}
}
res_2 = eor ^ res_1;
cout << res_1 << " " << res_2;
}
};
对数器
对数器用来验证某一种算法(设要检测的算法为A)是否正确。首先实现一个随机样本产生器,使用另一种实现简单的算法(设该种算法为B)与A运行相同的随机样本,观察得到的结果是否一致。若不一致,打印样本或断点调试进行人工干预,改进算法A或算法B。若随机样本的数量很多时A和B的运行结果仍然相同,可以认为方法A时正确的。下面是验证排序算法时使用的计数器,这里可以使用冒泡或选择排序作为comparator。
//for test
//rand()返回一随机数值的范围在0至RAND_MAX 间。RAND_MAX的范围最少是在32767之间(int)。
//用unsigned int 双字节是65535,四字节是4294967295的整数范围。0~RAND_MAX每个数字被选中的机率是相同的。
static int getRandlyNum(){
srand((unsigned int)(time(NULL)));
return rand();
}
static int getRandlyNum(int maxSize){ //返回 0 到 maxSize 之间的随机整数
srand((unsigned int)(time(NULL)));
return rand() % maxSize;
}
static int getRandlyNum(int a,int b){//返回 a 到 b 之间的随机整数
srand((unsigned int)(time(NULL)));
return rand()% (b - a) + a;
}
static double getRandlyDoubleNum(){ //返回0到1之间的小数
srand((unsigned int)(time(NULL)));
return rand() / double(RAND_MAX);
}
static int* generateRandlyArray(int maxSize,int maxValue,int& length){
length = getRandlyNum(2,maxSize);
int* arr = new int(length);
int i = 0;
srand((unsigned int)(time(NULL)));
for( ; i < length; i++){
arr[i] = rand() % maxSize;
}
return arr;
}
static int* copyArray(int* arr){
int i = 0;
int length = sizeof(arr) / sizeof(arr[0]);
int* res = new int(length);
for( ; i < length; i++){
res[i] = arr[i];
}
return res;
}
static bool isEqual(int* arr_1,int* arr_2){
if( arr_1 == NULL || arr_2 == NULL || (sizeof(arr_1) / sizeof(arr_1[0])) != (sizeof(arr_2) / sizeof(arr_2[0]) ))
return false;
int i = 0;
for( ; i < (sizeof(arr_1) / sizeof(arr_1[0])); i++){
if(arr_1[i] != arr_2[i])
return false;
}
return true;
}
static void printArray(int* arr){
if(arr == NULL)
return;
int length = sizeof(arr) / sizeof(arr[0]);
int i = 0;
for( ; i < length; i++){
cout << arr[i] << " ";
}
cout << endl;
}
递归
递归的实质是依靠系统栈来实现的。
可以将递归在系统栈中的执行顺序以二叉树的形式来表现。
计算递归算法的时间复杂度可以使用master公式。
master公式:T(N) = aT(N / b) + O(N^d) 其中,a表示实现该算法中有几个子问题,N/b 表示下一步子问题的数据规模,O(N^d)表示子问题外的时间复杂度。
如果 log(b,a) > d, 算法的时间复杂度为 O(N^log(b,a));
如果 log(b,a) = d, 算法的时间复杂度为 O(N^d*logN);
如果 log(b,a) < d, 算法的时间复杂度为 O(N^d);
如使用递归算法查找数组中的最大值。
public class GetMax {
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);
}
}
该算法中,a = 2, N/ b = N / 2, 子问题外的时间复杂度为O(1)。因为如果 log(b,a) = log(2,2) = 1 > d = 0, 算法的时间复杂度为 O(N));
需要注意的是,使用master公式的前提是子问题的规模必须相等。
子问题不相等的递归算法有BFPRT算法。
补充阅读::www.gocalf.com/blog/algorithm-complexity-and-master- theorem.html