Skip to content

Latest commit

 

History

History
515 lines (417 loc) · 13.9 KB

基础算法学习(一).md

File metadata and controls

515 lines (417 loc) · 13.9 KB

基础算法学习(一)

对数器

什么叫对数器呢?

我们可以通过一个事例来了解它,我们在学算法时,一般来说,会学到常见的排序算法(后面也会涉及到),我们在写完排序代码后需要对代码进行验证其正确性,比较常用的做法就是我们自己定义一个数组,然后运行我们的代码,然后我们逐一看结果是否按序排好,这种方式缺点就是不能将所有可能的情况全都覆盖,可能看起来我们的输出的结果没问题,其实在特殊情况下,可能会存在问题。对数器就是生成一个随机数组(数组大小,数组元素范围完全随机),然后将随机数组进行复制,一个数组用我们来进行排序,另一个数组我们一个绝对正确的排序算法来进行排序,将两组数据进行比较,一般来说我们会生成多个数组来进行排序,最终所有的数组排序正确才返回正确,由于数组是随机且大小、元素值都不唯一,而且数组是多组,因此如果再这种情况下返回正确的结果,那么我们就认为我们的算法是正确的。

下面给出排序算法的对数器实现:

首先,我们定义随机数组(统计学中我们可以叫做随机样本)的的实现:

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

这个简单的实现我们可以使用java的系统函数来解决也可以:

public static int[] copyArray(int[] arr) {
	if (arr == null) {
		return null;
	}
	return Arrays.copyOf(arr,arr.length);
}

下面来定义对数器的比较算法,这个也比较简单,也是调用java的系统方法:

public static void comparator(int[] arr) {
	Arrays.sort(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;
}

同时我们需要定义我们的自己的排序实现,比如叫sort()方法

public void sort(int[] arr){
    //实现逻辑
}

下面定义主函数:

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);
		sort(arr1);
		comparator(arr2);
		if (!isEqual(arr1, arr2)) {
			succeed = false;
			break;
		}
	}
	System.out.println(succeed ? "Yes!" : "No!");
}

上面的主函数的意思是,我们生成随机数组的元素大小为(-100,100)之间,数组大小在[0,100),我们比较的组数是500000,如果所有结果都正确就打印输出Yes!,如果有一组不满足就直接输出No!

时间复杂度、额外空间复杂度、稳定性

时间复杂度比较简单的定义,一个算法流程中,常数操作数量的指标,这个指标叫做O,big O。具体为,如果常数操作数量的表达式中, 只要高阶项,不要低阶项,也不要高阶项系数之后,剩下的部 分记为f(N),那么该算法的时间复杂度为O(f(N))。

额外空间复杂度:是程序运行所以需要的额外消耗存储空间,比如我们申请的几个变量,我们的空间复杂度就是O(1) ,再比如,一般的递归算法就要有o(n)的空间复杂度了,简单说就是递归集算时通常是反复调用同一个方法,递归n次,就需要n个空间。

我们再来看一看稳定性:我们可以用排序简单的理解稳定性:

比如我们有如下数组:

Basic algorithm practice 1

方框里代表元素的值,底下代表元素的索引值

如果我们算法是稳定的,我们排完序的结果:

Basic algorithm practice 1

如果我们算法是不稳定的,我们排完序的结果:

Basic algorithm practice 1

我们很容易的得到定义:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,ri=rj,且ri在rj之前,而在排序后的序列中,ri仍在rj之前,则称这种排序算法是稳定的;否则称为不稳定的。

我们需要注意的是,我们不能仅仅通代码中套了几个for(假设为m个for)循环就直接推出时间复杂度是O(N^m)。

冒泡排序

时间复杂度O(N^2),额外空间复杂度O(1),实现可以做到稳定性。

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

/**
 * 不需要申请变量(省空间)
 * 位运算速度非常快
 * i和j不能是一个变量,要不然最后为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];
}

插入排序

时间复杂度O(N^2),额外空间复杂度O(1),实现可以做到稳定性

public static void insertionSort(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);
        }
    }
}

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

选择排序

时间复杂度O(N^2),额外空间复杂度O(1),实现可以做到稳定性

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

荷兰国旗问题

荷兰国旗有三横条块构成,自上到下的三条块颜色依次为红、白、蓝。现有若干由红、白、蓝三种颜色的条块序列,要将它们重新排列使所有相同颜色的条块在一起。本问题要求将等于p的放在中间,小于P的放在左边,大于p的放在右边,最后返回中间位置的左边索引和右边索引。假设数组的元素只有0,1,2

public class NetherlandsFlag {

	public static int[] partition(int[] arr, int l, int r, int p) {
		int less = l - 1;
		int more = r + 1;
		while (l < more) {
			if (arr[l] < p) {
				swap(arr, ++less, l++);
			} else if (arr[l] > p) {
				swap(arr, --more, l);
			} else {
				l++;
			}
		}
		return new int[] { less + 1, more - 1 };
	}

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

	public static int[] generateArray() {
		int[] arr = new int[10];
		for (int i = 0; i < arr.length; i++) {
			arr[i] = (int) (Math.random() * 3);
		}
		return arr;
	}

	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[] test = generateArray();

		printArray(test);
		int[] res = partition(test, 0, test.length - 1, 1);
		printArray(test);
		System.out.println(res[0]);
		System.out.println(res[1]);

	}
}

运行结果:

0 2 0 2 0 0 2 1 0 1 
0 0 0 0 0 1 1 2 2 2 
5
6

随机快速排序(荷兰国旗升级版)

时间复杂度O(N*logN),额外空间复杂度O(logN),常规实现做不到稳定 性。 注意: 1,快速排序中,额外空间复杂度最低为O(logN) 2,快速排序,可以做到稳定性的实现,但是非常难,不需要掌握 3,荷兰国旗问题的实现,和快速排序中的改进

随机快排的意义在于,我们可能选的划分值可能选择了比较大的值或者比较小的值,这就导致了时间复杂度可能达到O(N^2)

public static void quickSort(int[] arr) {
		if (arr == null || arr.length < 2) {
			return;
		}
		quickSort(arr, 0, arr.length - 1);
	}

public static void quickSort(int[] arr, int l, int r) {
	if (l < r) {
	    //随机快速排序
		swap(arr, l + (int) (Math.random() * (r - l + 1)), r);
		int[] p = partition(arr, l, r);
		quickSort(arr, l, p[0] - 1);
		quickSort(arr, p[1] + 1, r);
	}
}

public static int[] partition(int[] arr, int l, int r) {
	int less = l - 1;
	int more = r;
	while (l < more) {
		if (arr[l] < arr[r]) {
			swap(arr, ++less, l++);
		} else if (arr[l] > arr[r]) {
			swap(arr, --more, l);
		} else {
			l++;
		}
	}
	swap(arr, more, r);
	return new int[] { less + 1, more };
}

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

Master公式

一个数组记为A,有序;另一个数组记为B,无序; 请打印B中的所有不在A中的数;A数组长度为N,B数组长度为M; 我们的实现时间复杂度O(M*logN) 估计一个算法流程的复杂度,需要对流程的细节彻底知晓,但是对于递 归函数,有一个重要内容:这是一个估计递归行为复杂度的公式,但是 要求递归行为中,每次递归的规模是固定的。

如何使用master公式? T(N) = aT(N/b) + N^d

如果 1,log(b,a) > d -> T(N) 的复杂度为 N^(log(b,a)) 2,log(b,a) == d -> T(N) 的复杂度为 N^d * logN 3,log(b,a) < d -> T(N) 的复杂度为 N^d

我们上面的随机快排就可以用Master公式来进行求解时间复杂度: 我们可以很轻松的得到:T(n) = 2T(n/2)+O(n) 所以a=2,b=2,d=1 所以满足第二种,T(n)=n^dlogn=nlogn

补充阅读:http://www.gocalf.com/blog/algorithm-complexity-and-master-theorem.html

堆排序

堆排序的核心是堆的建立(大根堆)和堆的调整

时间复杂度O(N*logN),额外空间复杂度O(1),实现不能做到稳定性 关键步骤:heapInsert, heapify,堆的扩大和缩小操作 注意: 1,堆排序中,建立堆的操作O(N),堆调整的操作时间复杂度为O(log(N)) 2,堆排序的核心数据结构:堆,也可以说是优先级队列

public static void heapSort(int[] arr) {
	if (arr == null || arr.length < 2) {
		return;
	}
	for (int i = 0; i < arr.length; i++) {
		heapInsert(arr, i);
	}
	int size = arr.length;
	swap(arr, 0, --size);
	while (size > 0) {
		heapify(arr, 0, size);
		swap(arr, 0, --size);
	}
}

public static void heapInsert(int[] arr, int index) {
	while (arr[index] > arr[(index - 1) / 2]) {
		swap(arr, index, (index - 1) / 2);
		index = (index - 1) / 2;
	}
}

public static void heapify(int[] arr, int index, int size) {
	int left = index * 2 + 1;
	while (left < size) {
		int largest = left + 1 < size && arr[left + 1] > arr[left] ? left + 1 : left;
		largest = arr[largest] > arr[index] ? largest : index;
		if (largest == index) {
			break;
		}
		swap(arr, largest, index);
		index = largest;
		left = index * 2 + 1;
	}
}

归并排序

时间复杂度O(N*logN),额外空间复杂度O(N),实现可以做到稳定性 注意: 1,库函数中排序的实现是综合排序,比如插入+快速;比如为了稳定性, 排序算法往往是快排+堆排序 2,归并排序和快速排序,都一定存在非递归的实现 3,归并排序,存在额外空间复杂度O(1)的实现,但是非常难,你不需要 掌握 4,归并排序的扩展,小和问题,逆序对

public static void mergeSort(int[] arr) {
    if (arr == null || arr.length < 2) {
        return;
    }

    mergeSort(arr, 0, arr.length - 1);

}

public static void mergeSort(int[] arr, int l, int r) {
    if (l == r) {
        return;
    }
    int mid = l + ((r - l) >> 1);
    mergeSort(arr, l, mid);
    mergeSort(arr, mid + 1, r);
    merge(arr, l, mid, r);
}

public static void merge(int[] arr, int l, int mid, int r) {
    int[] help = new int[r - l + 1];
    int s1 = l;
    int e1 = mid;
    int s2 = mid + 1;
    int e2 = r;

    int index = 0;
    while (s1 <= e1 && s2 <= e2) {
        help[index++] = arr[s1] > arr[s2] ? arr[s2++] : arr[s1++];
    }
    while (s1 <= e1) {
        help[index++] = arr[s1++];
    }

    while (s2 <= e2) {
        help[index++] = arr[s2++];
    }

    for (int i = 0; i < help.length; i++) {
        arr[i + l] = help[i];
    }
}

二分查找

二分查找的时间复杂度为O(logn)

public class BiSearch {

    public static int biSearch(int[] arr ,int p){
        int l = 0;
        int r = arr.length-1;
        if (arr[l]==p){
            return l;
        }else if (arr[r] == p){
            return r;
        }

        while (l<r){
            int mid = l+((r-l)>>1);
            if (arr[mid]==p){
                return mid;
            }else if (arr[mid]<p){
                l = mid+1;
            }else {
                r = mid-1;
            }
        }

        return -1;
    }

    public static void main(String[] args) {
        int[] arr = {1,2,5,7,12,14,53,100};
        System.out.println(biSearch(arr,2));
        System.out.println(biSearch(arr,53));
        System.out.println(biSearch(arr,10));
    }

}

运行结果:

1
6
-1