1、快速排序
时间复杂度O(N*logN),额外空间复杂度O(logN)
1.1 简单分析
这里只讲快排的完善版本:三项切分的随机快排
将一个数组分为小于一个数,等于一个数,大于一个数的三部分(主要是下面的partition函数来实现),为了节省变量,默认取最右边的数(这里用随机交换处理了一下,防止复杂度对数据有依赖),然后遍历数组,递归。
1.2快排的复杂度分析:
常数项很少,在时间复杂度都为O(nlogn)(因为涉及到了递归和master公式,详见上一篇博文)的时候,拼常数项,快排(随机快排)的额外空间复杂度是O(logN),空间浪费在要记住断点,patition,相当于二分查找,最差情况是O(N)(每次断电都在最右或者最左,所以这里用数组中随机的一个数和最右边的数交换,防止这种情况出现)
在工程上,快排不是递归的版本(工程上不会存在递归行为)
注意,这里代码有个坑:
在交换数组中两个数的时候,本来想装高级用一下异或交换,结果出来全是0,焦头烂额,因为这个异或是有坑的(敲黑板!!!),一个数异或自己,就是0,在这个快排中,当【swap(arr, ++less, lo++);】的时候,会出现自己和自己交换,这个时候用异或就会导致这个数变成0,所以啊,还是老老实实用temp交换吧
(参考文章:用异或进行两个数交换的陷阱)
具体代码如下:
package cn.nupt.sort;
import java.util.Arrays;
/**
* @Description: 快速排序(随机快排)
*
* @author PizAn
* @date 2019年1月23日 下午8:30:34
*
*/
public class QuickSort {
public static void quickSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
quickSort(arr, 0, arr.length - 1);
}
private static void quickSort(int[] arr, int lo, int hi) {
if(lo >= hi){
return;
}
swap(arr, lo + (int) ((hi - lo + 1) * Math.random()), hi); // 将hi换成是[lo,hi]里面的随机数,这就是随机快排的关键,这里的duble转int,比如10.9转了就变成10了
int[] p = partition(arr, lo, hi); // 将lo到hi分成三份,小于hi,等于hi和大于hi
// ,快排的第二个关键
quickSort(arr, lo, p[0] - 1);// 递归调用
quickSort(arr, p[1] + 1, hi);
}
private static int[] partition(int[] arr, int lo, int hi) {
int less = lo - 1;
int more = hi;
while (lo < more) {
if (arr[lo] < arr[hi]) { // 如果指到的是比hi小的数,和less后面一个数交换,然后less往后走一个囊括,lo也指下一个数
swap(arr, ++less, lo++); // less永远指向左边部分最后一个
} else if (arr[lo] > arr[hi]) { // 如果指到的是比hi大的数,和more指针换一下位置,然后more指针往前走一格,将交换后的比hi大的数囊括进去
swap(arr, --more, lo); // more永远指向右边部分的第一个数(注意:这个地方lo没有++,因为从--more换来的数还不知道是不是比arr[hi]小,还需要再来一次循环比较!!!!!)
} else {
lo++;
}
}
swap(arr, more, hi);// 因为我们是以arr[hi]为指标的,这个时候和more交换一下,more就知道了中间的一部分的最后一个数
return new int[] { less + 1, more };
}
private static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
2、堆排序
时间复杂度O(N*logN),额外空间复杂度O(1)
2.1 堆的相关概念(优先级队列结构,就是堆结构)
完全二叉树:满二叉树,或者从左至右补齐的二叉树,堆就是完全二叉树。
堆又分为大根堆和小根堆:
- 大根堆:在这颗树中,任何一颗子树,最大值就是头部
- 小根堆:在这颗树中,任何一颗子树,最小值就是头部
2.2 堆排序
堆之所以可以对数组进行排序,是因为数组可以表示成完全二叉树(即从左至右补齐的二叉树)(注:这是为了方便描述,脑补的,实际上没有生成),可以通过下面三个公式找到其左右孩子和父节点(0的父节点是自己)
- 对于数组中第i个数的左节点:2 * i + 1
- 对于数组中第i个数的右节点:2 * i + 2
- 对于数组中第i个数的父节点:( i - 1 ) / 2
堆的两个主要方法:
- heapInsert:一个节点如果比它的头节点大,上浮(一般用来将数组构建为大根堆)
(建立一个大根堆的时间复杂度为O(N)) - heapify:下沉,一个头节点如果变小了,那么就重新调整这个堆,将小的数下沉,一般是用来堆排序的时候,伴随着堆size的减小和排序的进行
(时间复杂度为O(NlogN),所以堆排的时间复杂度为O(N + NlogN) = O( NlogN))
堆排序的主要步骤是:
- 将数组变成大根堆(heapInsert),这个时候0指针处就是数组中最大的数
- 0和数组最后一个数交换,堆的size减小一,就找到了数组最大的数
- 剩下的heapify,将0指针处的数下沉,重新构建大根堆
- 如此往复,得到从小到大的排序数组
在下面的代码中,注意,在比较index的左右两个子节点大小并把largest的名号赋予最大的一个时,下面两个代码是有区别的!!!!下面的是正确的,因为在右节点在超出size的时候,largest就是左节点了
int largest = left + 1 < size && arr[left] > arr[left + 1] ? left : left + 1;
int largest = left + 1 < size && arr[left + 1] > arr[left] ? left + 1 : left;//左右孩子哪个大,谁的下标就是largest
完整代码和注释如下(这里加上了对数器,作测试用,具体看前面的博文):
package cn.nupt.sort;
import java.util.Arrays;
/**
* @Description: 堆排序
*
* @author PizAn
* @date 2019年1月24日 下午2:25:57
*
*/
public class HeapSort {
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);
}
}
// 将index上浮到最大,將数组变成大根堆形式
private static void heapInsert(int[] arr, int index) {
while (arr[index] > arr[(index - 1) / 2]) {
swap(arr, index, (index - 1) / 2);
index = (index - 1) / 2;
}
}
// 将index下沉到最小,在交换了0和最后一个元素后,重新调整,变成 大根堆
private static void heapify(int[] arr, int index, int size) {
int left = 2 * index + 1;
//在index、index的左右子节点中找到最大的那个
while (left < size) {
// 在index的左右节点比出大小
int largest = left + 1 < size && arr[left + 1] > arr[left] ? left + 1 : left;// 左右孩子哪个大,谁的下标就是largest
// 左右子节点的胜者再和index比较,如果还是index大,那么就不要下沉,直接退出,如果比index大,那么下沉,
// 在这个堆排序中,0与最后一个元素交换后,index初始化为0,将0处的元素下沉
largest = arr[index] > arr[largest] ? index : largest;
if (largest == index) {
break;
}
// 将index和largest交换,也就是将小的下沉了
swap(arr, index, largest);
// index指向最大的,这里肯定是之前的index的子节点了
index = largest;
left = 2 * index + 1;
}
}
private static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
==========================for test======================
public static void comparator(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
Arrays.sort(arr);
}
public static int[] generateRandonArray(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 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 < arr2.length; i++) {
if (arr1[i] != arr2[i]) {
return false;
}
}
return true;
}
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 int[] copyArray(int[] arr) {
int[] arrSup = new int[arr.length];
for (int i = 0; i < arrSup.length; i++) {
arrSup[i] = arr[i];
}
return arrSup;
}
public static void main(String[] args) {
int loopTimes = 10000;
int maxSize = 5000;
int maxValue = 5000;
boolean flag = true;
for (int i = 0; i < loopTimes; i++) {
int[] arr1 = generateRandonArray(maxSize, maxValue);
int[] arr2 = copyArray(arr1);
heapSort(arr1);
comparator(arr2);
if(!isEqual(arr1, arr2)){
flag = false;
printArray(arr1);
printArray(arr2);
break;
}
}
System.out.println(flag ? "good job!" : "oh no");
}
}
3、堆结构应用:获取最小的K个数、获取中位数
思路:两种堆:大根堆,存k个数;用小根堆,弹k次 (这里取最小的K个数,创建大根堆, 取最大的K个数,创建小根堆)
最小堆和最小堆的时间复杂度都是n*logn,但是最小堆在k个数之后添加的话需要调整堆, 但是最大堆只要每次和顶端的比较就可以。另外最小堆不能解决数字动态增加的情况。
下面用大根堆来获取一个数组中最小的k个数(这里重复的即相同的是否算一个数可以在后面的判断语句中修改)
package cn.nupt;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.PriorityQueue;
/**
* @Description: 输入n个整数,找出其中最小的K个数。例如输入4,5,1,6,2,7,3,8这8个数字,则最小的4个数字是1,2,3,4,。
*
* @author PizAn
* @Email pizan@foxmail.com
* @date 2019年3月11日 下午1:17:03
*
*/
public class Q_pp_GetLeastNumbers {
public ArrayList<Integer> GetLeastNumbers_Solution(int[] input, int k) {
// 解析:两种方法:大根堆,存k个数 小根堆,弹k次 (这里 取最小的K个数,创建大根堆, 取最大的K个数,创建小根堆)
// 最小堆和最小堆的时间复杂度都是n*logn,但是最小堆在k个数之后添加的话需要调整堆,
// 但是最大堆只要每次和顶端的比较就可以。另外最小堆不能解决数字动态增加的情况。
if (input == null) {
return null;
}
ArrayList<Integer> list = new ArrayList<Integer>();
if (k == 0 || k > input.length) {
return list; // 这里就是返回""
}
// 取最小的K个数,建立一个大根堆(这里重写了比较器,默认是创建小根堆)
PriorityQueue<Integer> maxHeep = new PriorityQueue<Integer>(new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o2 - o1;
}
});
// 遍历数组,往堆里放东西
for (int i = 0; i < input.length; i++) {
if (maxHeep.size() < k) { // 如果堆里面的数没有k个,入堆
maxHeep.offer(input[i]);
} else {
if (maxHeep.peek() > input[i]) { // 如果堆顶元素大于arr[i],弹出堆顶元素,将arr[i]入堆
maxHeep.poll();
maxHeep.offer(input[i]);
}
}
}
//对于堆的遍历,这里可以用迭代器,也可以再开辟一个ArrayList,并将maxHeep放到ArrayList的构造方法里
//方法一:ArrayList构造函数
/*ArrayList<Integer> list2 = new ArrayList<Integer>(maxHeep);
return list2;*/
//方法二:迭代器
/*
Iterator<Integer> iter = maxHeep.iterator();
while (iter.hasNext()) {
list.add(iter.next());
}
return list;*/
//方法三,直接增强for循环
for(Integer inte : maxHeep){
list.add(inte);
}
return list;
}
}
堆还有很多用处,举个例子,不断获得随机数,在某个时间段我想知道我已经获得的数的中间值
- 如果按照之前的思路,我们要先找个集合存放这些数,如数组,然后用O(NlogN)复杂度的排序算法来排序,再计算中间值
- 如果用堆,建立一个大根堆,建立一个小根堆,然后偶数个放在大根堆里,奇数个放在小根堆里(不是直接放,而是先放到小/大根堆里,然后弹出一个放到大/小根堆里,这样就能保证小根堆里全是大数,大根堆里全是小数了),始终保持大根堆的占N/2的较小数据,小根堆的占N/2大的数据,随时可以知道中间数,其加入数和弹出数的时间复杂度只跟树的层数有关,就是log(N),即弹出表头,然后重新下沉建立大的时间,试想,如果我现在获得了40多亿数据,用堆,算下来就是log(40亿)= 32
下面是具体代码:
package cn.nupt;
import java.util.Comparator;
import java.util.PriorityQueue;
/**
* @Description: 获取数据流中的中位数
*
* @author PizAn
* @Email pizan@foxmail.com
* @date 2019年3月11日 下午3:48:54
*
*/
public class Q_pp_GetMedian {
private PriorityQueue<Integer> minHeep;
private PriorityQueue<Integer> maxHeep;
int count = 0;
public Q_pp_GetMedian() {
minHeep = new PriorityQueue<Integer>();
maxHeep = new PriorityQueue<Integer>(new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o2 - o1;
}
});
}
public void Insert(Integer num) {
//这个地方是1,然后下面就先把数放在了max中,所以如果总数是奇数(这个跟count放在前后没关系,反正是要++),那肯定是man中多了一个数
count++;
if (count % 2 == 0) { //偶数个放到 min
maxHeep.offer(num);
minHeep.offer(maxHeep.poll());
} else { //奇数个放到 max
minHeep.offer(num);
maxHeep.offer(minHeep.poll());
}
}
public Double GetMedian() {
if (count % 2 == 1) {// 入堆是奇数,不用想,肯定是大根堆里面的数多一个
// return (double) maxHeep.peek();
return new Double(maxHeep.peek());
} else {
return ((double) (maxHeep.peek() + (double) minHeep.peek()) / 2);
// return new Double((maxHeep.peek() + minHeep.peek()) / 2);
}
}
}