排序算法
记录左神算法学习笔记之排序算法
1.冒泡排序
/**
* 方法:冒泡排序
* 复杂度; O(n^2)
* 稳定性:稳定
* 说明:遍历过程中把较大的值往后移动。
*/
public static void bubbleSort(int[] arr) {
if (arr == null || arr.length < 2) return;
//0 ~ N-1 -> 0 ~ N - 2
for (int i = arr.length - 1; i > 0; i--) {
for (int j = 0; j < i; j++) {
//左边元素大于右边,将大的交换到右边去
if (arr[j] > arr[j + 1]) {
swap(arr, j, j + 1);
}
}
}
}
2.选择排序
/**
* 方法:选择排序
* 算法复杂度:O(N^2)
* 稳定性:不稳定
* 不稳定例子: (7) 2 5 9 3 4 [7] 1. (7)排到[7]后了
* 每次查找最小的值放入到数组中
*/
public static void selectSort(int[] arr) {
if (arr == null || arr.length < 2) return;
//每次选择最小的元素放在对应的位置上
for (int i = 0; i < arr.length - 1; i++) {
//在 i ~ n - 1位置查找最小值
int minIndex = i;
for (int j = i + 1; j < arr.length; j++) {
//找到一个更小的元素,更新 minIndex
minIndex = arr[j] < arr[minIndex] ? j : minIndex;
}
if (minIndex != i) swap(arr, minIndex, i);
}
}
3.插入排序
/**
* 方法: 插入排序
* 算法复杂度: O(N^2)
* 稳定性:稳定
* 1.每次保证 0 ~ i是已经排序好的
* 2.每次从后往前查询插入的位置
*/
public static void insertSort(int[] arr) {
if (arr == null || arr.length < 2) return;
//0~0 是有序的,想要 0~i是有序的
for (int i = 1; i < arr.length; i++) {
// j 是从 i - 1 开始的,就是已经排好序的最后一个位置开始的,然后和即将插入的元素相比,如果最后一个元素比
//待插入的元素 arr[j + 1] 大,那需要将该元素往数组前移动
for (int j = i - 1; j >= 0 && arr[j] > arr[j + 1]; j--) {
swap(arr, j, j + 1);
}
}
}
4.归并排序
/**
* 方法:归并排序
*/
public static void mergeSort(int[] arr) {
if (arr == null || arr.length < 2) return;
process(arr, 0, arr.length - 1);
}
/**
* 归并排序的递归过程
* arr[L...R]范围上,变成有序的
* 时间复杂度计算公式:master T(N) = a(N/b) + O(N^d)
* 复杂度计算过程:子问题规模 N / 2,调用 2 次,除了递归之外的时间复杂度为 merge 的时间复杂度,为O(N)。a = 2,b = 2,d = 1满足master第一条 logb^a == d规则
* T(N) = 2T(N/2) + O(N) => O(N*logN)
* 时间复杂度为: O(nlogn)
* 空间复杂度为:O(n)
* 稳定性:稳定
*/
public static void process(int[] arr, int l, int r) {
if (l == r) return;
int mid = l + ((r - l) >> 1);
//让左边有序
process(arr, l, mid);
//让右边有序
process(arr, mid + 1, r);
//合并两个有序的数组
merge(arr, l, mid, r);
}
/**
* 合并两个有序数组
*/
public static void merge(int[] arr, int l, int m, int r) {
int[] temp = new int[r - l + 1];
//定义左边数组指针 p1,右边数组指针p2,temp 数组指针p
int p = 0, p1 = l, p2 = m + 1;
while (p1 <= m && p2 <= r) {
temp[p++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++];
}
while (p1 <= m) {
temp[p++] = arr[p1++];
}
while (p2 <= r) {
temp[p++] = arr[p2++];
}
//将temp数组赋值到arr数组中
if (temp.length >= 0) System.arraycopy(temp, 0, arr, l, temp.length);
}
1.求小和问题?
在一个数组中,一个数左边比它小的数的总和,叫做小和,所有数的小和累加起来,叫做数组的小和。求数组的小和。例如[1, 3, 4, 2, 5]
1左边比1小的数:没有 3左边比3小的数:1 4左边比4小的数:1、3 2左边比2小的数为:1 5左边比5小的数为:1、3、4、2 所以该数组的小和为:1+1+3+1+1+3+4+2 = 16
了解归并流程后,在merge过程中,产生小和。规则是左组比右组小,则产生小和。
注意:
当左组和右组数相等的时候,拷贝右组的数,不产生小和(区别于归并排序,我们是拷贝的左数组);
当左组的数大于右组的时候,拷贝右组的数,不产生小和(因为此时如果拷贝左数组,就导致左组当前元素没有和右组的后面元素进行比较,会丢失情况,请特别注意)。
实质是把找左边比本身小的数的问题,转化为找这个数右侧有多少个数比自己大,在每次merge的过程中,一个数如果处在左组中,那么只会去找右组中有多少个数比自己大。
/**
* 左神:小和问题
* 题目:在一个数组中,一个数左边比它小的数的总和,叫做小和,所有数的小和累加起来,叫做数组的小和。求数组的小和。例如[1, 3, 4, 2, 5]
* 1左边比1小的数:没有
* 3左边比3小的数:1
* 4左边比4小的数:1、3
* 2左边比2小的数为:1
* 5左边比5小的数为:1、3、4、2
* 所以该数组的小和为:1+1+3+1+1+3+4+2 = 16
*/
public static int smallSum(int[] arr) {
if(arr == null || arr.length < 2) return 0;
return process1(arr,0,arr.length - 1);
}
public static int process1(int[] arr, int l, int r) {
//只有一个数,不存在右组,小和为0
if(l == r) return 0;
int mid = l + ((r - l) >> 1);
// 左侧merge的小和+右侧merge的小和+整体左右两侧的小和
return process1(arr,l,mid) + process1(arr,mid + 1,r) + merge1(arr,l,mid,r);
}
/**
* 归并排序的merge过程,在这个过程中记录小和
*/
public static int merge1(int[] arr, int l, int m, int r) {
int[] help = new int[r - l + 1];
int p = 0, p1 = l, p2 = m + 1;
int ans = 0;
while (p1 <= m && p2 <= r){
//当前的数是比右组小的,产生右组当前位置到右组右边界数量个小和,累加到 ans。否则 ans 加 0
ans += arr[p1] < arr[p2] ? (r - p2 + 1) * arr[p1] : 0;
//归并数组,注意这里的区别,左边小拷贝左边,大于等于的选择拷贝右边
help[p++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++];
}
while (p1 <= m){
help[p++] = arr[p1++];
}
while (p2 <= r){
help[p++] = arr[p2++];
}
//将temp数组赋值到arr数组中
if (help.length >= 0) System.arraycopy(help, 0, arr, l, help.length);
return ans;
}
2.求逆序对?
5.快排
1.荷兰国旗问题
荷兰国旗1.0版本:
Partion过程:
- 给定一个数组arr,和一个整数 num。请把小于等于num的数放在数组的左边,大于num的数放在数组的右边(不要求有序)。要求额外空间复杂度为O(1),时间复杂度为O(N)。例如[5,3,7,2,3,4,1],num=3,把小于等于3的放在左边,大于3的放在右边
- 设计一个小于等于区域,下标为 -1。
- 开始遍历该数组,如果arr[i]<=num,当前数和区域下一个数交换,区域向右扩1,i++
- arr[i] > num, 不做操作,i++
荷兰国旗问题:
给定一个数组,和一个整数num。请把小于num的数放在数组的左边,等于num的放中间,大于num的放右边。要求额外空间复杂度为O(1),时间复杂度为O(N)。[3,5,4,0,4,6,7,2],num=4。实质是经典荷兰国旗问题。
1.需要维护三个区域,左边的小于区域,中间的等于区域,右边的大于区域。用图中的0,1,2来表示。
2.初始化的时候红-白-蓝 三色是乱序的,所以此时的两条线我们是不是可以看成在最两侧
3.我们发现此时 C 等于0。是不是意味着,我们应把这个元素放到 A 的左侧,所以我们移动 A线。当然,我们也需要移动一下 C 的位置
新的 C 位置处等于2,那是不是说明这个元素应该位于 B 的右侧。所以我们要把该位置的元素 和 B位置处的元素进行交换,同时移动B。
C 交换完毕后,C 不能向前移。因为C指向的元素可能是属于前部的,若此时 C 前进则会导致该位置不能被交换到前部。继续向下遍历。
1)若遍历到的位置为0,则说明它一定位于A的左侧。于是就和A处的元素交换,同时向右移动A和C。
2)若遍历到的位置为1,则说明它一定位于AB之间,满足规则,不需要动弹。只需向右移动C。
3)若遍历到的位置为2,则说明它一定位于B的右侧。于是就和B处的元素交换,交换后只把B向左移动,C仍然指向原位置。(因为交换后的C可能是属于A之前的,所以C仍然指向原位置)
/**
* 左神:荷兰国旗问题
* 题目:给定一个数组,和一个整数num。请把小于num的数放在数组的左边,等于num的放中间,
* 大于num的放右边。要求额外空间复杂度为O(1),时间复杂度为O(N)。[3,5,4,0,4,6,7,2],num=4。实质是经典荷兰国旗问题。
*/
public static void sortArray(int[] arr, int target) {
if (arr == null || arr.length < 2) return;
int left = 0, right = arr.length - 1, cur = 0;
while (cur <= right){
if (arr[cur] < target){
//交换到左区域,并且cur指针和left指针都移动
swap2(arr,left,cur);
cur++;
left++;
}else if (arr[cur] > target){
//此时应该将元素移到右边界。右边界指针移动,cur指针也不动,因为我不知道当前移过来的元素是否满足条件,需要继续判断
swap2(arr,cur,right);
right--;
}else {
//当前元素与目标元素相等,只用移动cur指针
cur++;
}
}
}
2.快排
思想:借助荷兰问题,我们从数组中选择一个元素,然后让数组以该元素分为左右两部分。分完之后,该元素在排序数组中的位置一定是确定下来的。
继续计算左边数组和右边数组相同的过程,然后就能将找到所有元素的位置。注意:因为Partion过程会存在元素的交换(将相同的7交换到数组的最后方,所以快排是不稳定的。)
在最差情况下,如果我们每次找的元素都是位于位于排序后数组的末尾,则此时快排的时间复杂度会递增到 O ( N 2 ) O(N^2) O(N2) ,最好情况,其他各种情况的出现概率为 1 / N 1/N 1/N。对于这 N 种情况,数学上算出的时间复杂度最终期望是 O ( N l o g N ) O(NlogN) O(NlogN)
/**
* 左神快排:基于荷兰问题上进一步排序
* 时间复杂度:O(NlogN)
* 空间复杂度:O(logN)
* 稳定性:不稳定
*/
public static void quickSort(int[] arr) {
quickSortHelper(arr, 0, arr.length - 1);
}
public static void quickSortHelper(int[] arr, int l, int r) {
if (l < r) {
//为了避免最坏的情况,我们随机算选择一个数作为划分值,将其放到数组的尾部,可以随机算去
// swap2(arr,l + (int)(Math.random() * (r - l + 1)),r);
//通常我们选中间元素就行
int mid = l + (r - l) / 2;
swap2(arr, mid, r);
int[] p = partition(arr, l, r);
quickSortHelper(arr, l, p[0] - 1);//小于区域
quickSortHelper(arr, p[1] + 1, r);//大于区域
}
}
/**
* partition过程,这是一个处理 arr[l,r]的过程
* 默认以 p = arr[r]做划分,分成三部分 <p ==p >p
* 返回的是中间等于区域(左边界,右边界),所以这里反回了一个长度为2的数组res
*/
public static int[] partition(int[] arr, int l, int r) {
int less = l - 1; // 小于 p 区域的边界
int more = r; // 大于 p 区域的边界
while (l < more) { // l表示当前操作的元素,而 arr[r] 为划分值
if (arr[l] < arr[r]) {
//当前元素小于划分值,应该放在左测区域,并且移动指针,因为less是前一个区域的边界
swap2(arr, ++less, l++);
} else if (arr[l] > arr[r]) {
//当前元素大于划分值,要分到右边界,此时交换元素,并且l指针不能动,因为我不确定当前交换的值是否满足条件
swap2(arr, --more, l);
} else {
//当前元素为划分值,则只用移动l指针
l++;
}
}
//将划分值放回右边区域的边界位置
swap2(arr, more, r);
//返回中间等于区域的左右边界
return new int[]{less + 1, more};
}
6.堆排序
1.大根堆和小根堆
基础性质:
其中 i i i 表示数组中的索引,将它对应成堆后的性子。
父节点索引: ( i − 1 ) / 2 (i -1)/2 (i−1)/2
左孩子索引:$ 2*i + 1$
右孩子索引: 2 ∗ i + 2 2*i + 2 2∗i+2
大根堆: a r r [ i ] > a r r [ 2 ∗ i + 1 ] & & a r r [ i ] > a r r [ 2 ∗ i + 2 ] arr[i] > arr[2*i + 1] \&\& arr[i] > arr[2*i+2] arr[i]>arr[2∗i+1]&&arr[i]>arr[2∗i+2]
小根堆: a r r [ i ] < a r r [ 2 ∗ i + 1 ] & & a r r [ i ] < a r r [ 2 ∗ i + 2 ] arr[i] < arr[2*i + 1] \&\& arr[i] < arr[2*i+2] arr[i]<arr[2∗i+1]&&arr[i]<arr[2∗i+2]
完全二叉树中如果每棵子树的最大值都在顶部就是大根堆
完全二叉树中如果每颗子树的最小值都在顶部就是小根堆
两个重要堆结构操作:
heapInsert
要构建一个大根堆,把所有的数依次添加到一个数组(下标从0开始)中去,每次添加一个数的时候,要去用找父亲节点的公式 p a r e n t = ( i − 1 ) / 2 parent = (i-1) / 2 parent=(i−1)/2找到父节点去比较,如果比父节点大就和父节点交换向上移动,移动后再用自己当前位置和父亲节点比较…,小于等于父节点不做处理。这样用户每加一个数,我们都能保证该结构是大根堆。
我们的调整代价实际上就是这颗树的高度层数, l o g N logN logN
heapify
删除了最大值,也就是 a r r [ 0 ] arr[0] arr[0]位置,之后我们把堆最末尾的位置调整到 a r r [ 0 ] arr[0] arr[0]位置,堆大小减一。让现在 a r r [ 0 ] arr[0] arr[0]位置的数找左右孩子比较…,进行
hearify
操作,让其沉下去。沉到合适的位置之后,仍然是大根堆。heapify的下沉操作,仍然是树的高度,logN
整个过程如下:
2.手写大堆:
/**
* 创建大根堆
*/
public class MaxHeap {
/**
* 存储堆元素数组
*/
private int[] heap;
/**
* 堆的大小限制,当然超过了会进行数组的两倍扩容
*/
private final int capacity;
/**
* 表示目前这个堆收集了多少个数,也表示添加的下一个数应该放在哪个位置
*/
private int heapSize;
public MaxHeap(int capacity) {
this.capacity = capacity;
heap = new int[capacity];
heapSize = 0;
}
public boolean isEmpty() {
return heapSize == 0;
}
public boolean isFull() {
return heapSize == capacity;
}
public void push(int value){
if (heapSize == capacity){
throw new RuntimeException("heap is full");
}
heap[heapSize] = value;
//调整堆结构,heapSize作为操作数组的指针
heapInsert(heap,heapSize++);
}
/**
* 从堆中取出堆顶元素,即返回最大值,并且在大根堆中,
* 把最大值删掉剩下的数,依然保持大根堆组织
*/
public int pop(){
if (heapSize == 0) {
throw new RuntimeException("heap is empty");
}
int ans = heap[0];
//这里的移除只是改变 heapSize 指针。至于数组的元素还存在对我们堆结构没有任何影响。因为我们在push操作中会覆盖当前值
//我们将堆中最后一个元素移动到堆顶执行heapify操作,下沉堆顶元素
swap(heap,0,--heapSize);
//执行下沉操作
heapify(heap,0,heapSize);
return ans;
}
public int peek(){
if (heapSize == 0) {
throw new RuntimeException("heap is empty");
}
return heap[0];
}
/**
* 向堆中插入元素
*/
private void heapInsert(int[] arr,int index){
//arr[index] 不比 arr[index父]大了,停止heapInsert过程
//注意当比较到 0 位置即堆顶时候,此时循环也不会进入了
while (arr[index] > arr[(index - 1) / 2]){
swap(arr,index,(index - 1) / 2);
//将指针移动到父节点,继续上升的过程
index = (index - 1) / 2;
}
}
/**
* 从index位置,往下看,不断的下沉,停的条件:
* 我的孩子都不再比我大;已经没孩子了
*/
private void heapify(int[] arr,int index,int heapSize){
//找到左孩子索引
int leftIndex = index * 2 + 1;
//这里如果左孩子越界,那右孩子更越界,右孩子等于左孩子+1
while (leftIndex < heapSize){
//下沉操作,父节点与左右孩子相比, 左右两个孩子中,谁大,谁把自己的下标给largest,因为大的节点得上升
// 选择右 -> (1) 有右孩子 && (2)右孩子的值比左孩子大才行
int largest = leftIndex + 1 < heapSize && arr[leftIndex + 1] > arr[leftIndex] ? leftIndex + 1 : leftIndex;
//左右孩子中最大值,和当前值父节点比较,谁大谁把下标给largest(当前,左,右的最大值下标)
largest = arr[largest] > arr[index] ? largest : index;
//index 位置上的数比左右孩子的数都大,已经无需下沉
if(index == largest) break;
//交换父节点和子节点中的较大孩子。周而复始进行
swap(arr,index,largest);
//更新父节点的index,即父节点往下沉
index = largest;
//继续看它的左右孩子是否需要调整
leftIndex = index * 2 + 1;
}
}
private void swap(int[] arr,int i,int j){
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
3.堆排序
/**
* 左神:堆排序
* 时间复杂度为:O(NlogN)
* 空间复杂度:O(1)
* 稳定性:不稳定
* 对于用户给的所有数据,我们先让其构建成为大根堆
* 对于0到N-1位置的数,我们依次让N-1位置的数和0位置的数(全局最大值)交换,此时全局最大值来到了数组最大位置,堆大小减一,
* 再heapify调整成大根堆。再用N-2位置的数和调整后的0位置的数交换,相同操作。直至0位置和0位置交换。每次heapify为logN,交换调整了N次
* <p>
* 堆排序额为空间复杂度为O(1),且不存在递归行为
*/
public static void heapSort(int[] arr) {
if (arr == null || arr.length < 2) return;
//1.将元素数组构建成堆事件复杂度为 0(NlogN)
for (int i = 0; i < arr.length; i++) {
heapInsert(arr, i);
}
//1.1拓展,如果直接给定一个数组,我们直接执行heapify操作构建大堆,从末尾开始看是否需要heapify
// for (int i = arr.length - 1; i >= 0;i--){
// heapify(arr,i,arr.length);
// }
//2.将堆顶元素移到末尾,堆的heapSize--;
int heapSize = arr.length;
swap2(arr, 0, --heapSize);
//开始下沉操作
while (heapSize > 0) {
heapify(arr, 0, heapSize);
swap2(arr, 0, --heapSize);
}
}
/**
* 向堆中插入元素,维持大堆结构
* arr[index]刚来的数,通过比较进行上升
*/
public static void heapInsert(int[] arr, int index) {
//与父节点相比,大于就上身,否则停止
while (arr[index] > arr[(index - 1) / 2]) {
swap2(arr, index, (index - 1) / 2);
index = (index - 1) / 2;
}
}
/**
* 下沉操作,将当前 index 位置下沉到合适的位置,保持堆结构稳定
*/
public static void heapify(int[] arr, int index, int heapSize) {
int left = index * 2 + 1;
while (left < heapSize) {
//1.找左右孩子中较大的一个
int largest = left + 1 < heapSize && arr[left + 1] > arr[left] ? left + 1 : left;
//2.将孩子与父节点进行比较,父节点比孩子小,则需要下沉
largest = arr[index] < arr[largest] ? largest : index;
//3.如果父节点的索引没变,则说明下层已经满足大堆结构了,直接break
if (largest == index) break;
//4.否则父节点与子节点交换
swap2(arr, index, largest);
//5.父节点指针移动,移动到孩子节点,接续判断孩子节点的树是否满足大堆结构,周而复始
index = largest;
left = index * 2 + 1;
}
}