算法整理(一)——排序算法及拓展

左神算法课笔记整理

冒泡排序

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)=aT(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正确

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值