左神算法课笔记整理
冒泡排序
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];
}
时间复杂度O(n²)
选择排序
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);
}
}
时间复杂度O(n²)
插入排序
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);
}
}
}
时间复杂度O(n²),与数据状况有关。若数组已有序可降至O(n)
递归时间复杂度估算——master公式
适用条件:子过程规模一样
若每次递归中的代价为
T
(
N
)
=
a
∗
T
(
N
b
)
+
O
(
N
d
)
T(N)=a*T(\frac{N}{b})+O(N^d)
T(N)=a∗T(bN)+O(Nd)
其中:
a:每次过程中调用子过程的次数
b:子过程的样本量
Nd:除去调用子过程外剩下的代价
则分以下三种情况
1.log(b,a)>d,复杂度O(Nlog(b,a))
2.log(b,a)==d,复杂度O(Nd*logN)
3.log(b,a)<d,复杂度O(Nd)
归并排序
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 m, int r) {
int[] help = new int[r - l + 1];
int i = 0;
int p1 = l;
int p2 = m + 1;
while (p1 <= m && p2 <= r) {
help[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++];
}
while (p1 <= m) {
help[i++] = arr[p1++];
}
while (p2 <= r) {
help[i++] = arr[p2++];
}
for (i = 0; i < help.length; i++) {
arr[l + i] = help[i];
}
利用master公式计算归并排序复杂度:
每次递归过程调用两次子过程,a=2
每次子过程的样本量均为父过程一半,b=2,满足适用条件
除此之外每次merge过程需要O(n)代价,d=1
log(b,a)=d=1,复杂度O(Nd*logN)=O(nlogn)
归并排序优于上面三个排序的根本原因:组内的排序被用于merge过程,而没有被浪费。
小细节:int mid = l + ((r - l) >> 1);比int mid = (l + r) >> 1更好,因为不会溢出
归并思想拓展:小和问题
问题描述:一个数组中每个数左边比当前小的数累加之和,叫做数组的小和,求一个数组的小和。
即小和=∑这个数*右边比它大的数字的个数
所以,关键在于我们如何求出每个数字右边有多少个数比它大。
思路:merge之前,左右两个子数组已经有序。在merge过程中,当左边子数组的每个数被放入help数组时,根据简单的下标变换可知右边子数组还有多少个数未放入help数组,这就是右边子数组比它大的数字个数,把它与数字相乘,并在整个过程中不断累加,最终得到小和。
注:merge过程中不用管左边子数组中剩余数字,因为左边子数组曾经也被划分为左右两部分,并由merge得到,所以这部分的小和已经被计算。
快速排序
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 };
}
时间复杂度O(nlogn),虽然是递归过程,但是不可以用master公式计算,因为每次递归的子过程样本量不一定相同,不满足公式适用条件。在最差情况下,每次选到的都是当前数组中最大或最小的数,此时时间复杂度增加到O(n²),不过算法在长期期望的情况下复杂度收敛于O(nlogn)。
其中partition过程是快排的核心,它负责把数组arr上的l到r中的数分成小于pivot、等于pivot、大于pivot三部分,这个问题也叫荷兰国旗问题。
详解:用cur指针划过数组,判断每个数字与pivot比较的大小情况:
1.当前数字小于pivot:与小于区域的下一个数交换,小于区域扩容,当前指针后移。
2.当前数字大于pivot:与大于区域的上一个数交换,大于区域扩容。
3.当前数字等于pivot:当前指针后移。
less、more、cur三个下标将数组划分为四部分,less和more一开始停在数组外,代表没有发现任何数大于或小于pivot。随着过程的进行,less左边的数全部小于pivot,more右边的数总大于pivot,在less和cur之间的是等于pivot的数,在cur和more之间的是待确定的数。
所以在遇到等于pivot的数时,cur直接右移也可以看做是等于pivot的区域扩容。less所在的位置必然是等于pivot的(取第一个数为基准的情况),所以遇到小于pivot的数,交换之后cur可以直接后移。而more位置的数是不确定的,所以遇到大于pivot的数时,交换后cur要留在原地判断换过来的数的大小情况。结束条件是cur>=more,此时不确定区域已不存在,过程结束。
partition过程返回一个长度为2的数组,分别代表小于pivot的右边界和大于pivot的左边界。子过程将在小于pivot的部分和大于pivot的部分分别进行。
优化:如果我们每次选择的pivot值都能尽量将小于、大于部分均分,此时的算法效率较高。所以我们可以先随机选择数组中的三个数,并取中间的数作为基准与第一个交换,这样一般情况下可以提高算法的效率。
堆排序
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(nlogn)
heapInsert:大根堆构建
heapify:调整堆
堆结构应用:求数组的中位数
中位数:可以根据大于它的数和小于它的数划分为两个相等或数量差不超过1的子数组。
将N个数逐一读入,大的N/2构成小根堆,小的N/2构成大根堆。用两个变量记录当前大根堆、小根堆中数的数量。若相差超过2,则将多的堆的堆顶元素移动到另一个堆,并更新数量。
这是一个实时处理算法,任意时间停止读入都可以得到当前情况下的正确结果。若此时两堆数量相等,则中位数=两堆顶之和/2,若相差1,则数字较多的堆的堆顶为中位数。
工程综合排序
实际的排序算法中往往不仅仅是单纯的某一种算法,而是集各家之所长,融合而成的算法。这种算法比起单独用一种算法更高效。
举例:
对于基础类型(int、double等)常常使用快排(不稳定)。
对于自定义类型常常使用归并排序(稳定,但是常数项高)。
当快排或堆排的分批长度小于60或长度小于60的数组排序,使用插排(虽然时间复杂度O(n²),但是常数项极低,在小数据量下要快于O(nlogn)
稳定/不稳定:
在实际业务中,我们可能有这样的情景:在某房屋中介的房源搜索中,我们先按价格排序,再按面积排序。如果是稳定的归排,那么对于同样面积的房屋,可以保持这部分的价格有序。而不稳定的快排是做不到这一点的。对于基本类型而言,哪个在前哪个在后没有区别,所以可以用常数项低的快排。
非基于比较的排序
这类排序以桶排序、计数排序、基数排序为代表,它们都是稳定排序,且时间复杂度为O(n),但是因为受限于数据状况,所以无法作为通解应用到所有方面。
桶:一种数据状况出现的词频
桶的应用:给定一个数组,求:如果对数组进行排序,相邻两数差值的最大值。要求时间复杂度O(n),且不能用非基于比较的排序。
1.N个数,准备N+1个桶,每个桶有一个boolean标志是否进过元素、并记录进入过的最小值、最大值。
2.遍历找出最小值min、最大值max、若min == max返回0。
3.从min到max等分为N+1份,对应N+1个桶。入桶时若boolean为false,改为true,同时更新最大值,最小值。min入0号桶,max入N号桶。
4.逐个读入数组中的数,并入桶。
5.从1号桶开始,对每个非空桶,其最小值与上一个相邻非空桶的最大值做差,差值更新为最大,最后的结果就是答案。
注:该方法只否定了一个桶内相邻数间差值最大的情况,因为桶内间隔必然小于桶间间隔,而且必然存在空桶。
Java中的比较器类
比较器:相当于C++的自定义类关系运算符重载
public static class <比较器名> implements Comparator <类名> {
@override
public int compare(<类名> o1, <类名> o2) {
return o1.<变量名> - o2.<变量名>;
}
}
使用:
Arrays.sort(<类数组>, new <比较器名>());
PriorityQueue<类名> <变量名> = new PriorityQueue<>(new <比较器名>());
其中PriorityQueue为优先级队列,是一个带权值观念的queue,插入元素时自动依照元素权值排列,取出元素时只能取出权值最大者。内部由堆结构维护。
对数器
对数器:用于验证某一方法是否正确
在实际编程中,很多题目是可以被暴力破解的,但是暴力破解的方法时间复杂度一般不好,而对数器可以利用暴力破解的方法验证某一方法是否正确:
0.有一个想测的方法a
1.实现一个绝对正确但是复杂度不好的方法b
2.实现一个随机样本产生器
3.实现比对的方法
4.把方法a、b比对很多次来验证方法a是否正确
5.若有一个样本使比对不一致,打印样本分析哪个方法出错,并修正
6.若样本数很多时比对测试仍然正确,则可以确定方法a正确