【数据结构与算法学习笔记】一、排序算法
一、排序算法
1. 异或运算^
二进制中相同为1,不同为0,可以理解为无进位相加
性质:
- 0 ^ N = N 、N ^ N = 0
- 交换律:a ^ b = b ^ a
- 结合律:a ^ b ^ c = a ^ (b ^ c) 运算顺序不影响结果
交换两个数(swap)
注意:只能在保证每个数都不一样的情况下使用,否则会把数字变成0(任何数异或自己等于0)
if (arr[i] == arr[j]){return;} //保证两个数不一样
public static void swap(int arr[], int i, int j){
arr[i] = arr[i] ^ arr[j]; // a = 甲 ^ 乙
arr[j] = arr[i] ^ arr[j]; //b = 甲 ^ 乙 ^ 乙 = 甲
arr[i] = arr[i] ^ arr[j]; //a = 甲 ^ 乙 ^ 甲 = 乙
}
异或例题:找出数组中出现奇数次的数
1、数组中有一个数出现了奇数次,其他出现了偶数次
public static int FindOddTimesNum1(int[] arr){
int oddNum = 0;
for (cur : arr){
oddNum ^= cur; //全部异或,得到结果
}
return oddNum;
}
2、数组中有两个数出现了奇数次,其他出现了偶数次
public static int FindOddTimesNum2(int[] arr){
int oddNum = 0;
for(cur : arr){
oddNum ^= cur;
}
//重复上一步,得到oddNum = a ^ b
//因为两个数不一样,所以其中必有一位是1
//提取出1的位置
int pos = oddNum & (~oddNum + 1)
//例 oddNum = 1001
//~oddNum = 0110
//~oddNum + 1 = 0111
//oodNum & (~oddNum + 1) = 0001
//这样提取出来就是最右边是1的一位还是1,其他位置上都为0
int oddNum2 = 0;
for(cur : arr){
if((cur & pos) == 0){ //因为pos其他位都是0,&结果一定是0不用管,如果结果=0那么说明这个数在这个1位置上也是0,可以改写成 (cur & pos) == pos
oddNum2 ^= cur;
}
System.out.println(oddNum2 + " " + (oddNum ^ oddNum2))
}
}
2. 简单排序
- 选择排序
public static void selectionSort(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++){ //i ~ N - 1上找最小值的下标
minIndex = arr[j] < arr[minIndex] ? j : minIndex;
}
swap(arr,i,minIndex); //交换
}
}
- 冒泡排序
public static void BubbleSort(int[] arr){
//去掉不合适的数组
if (arr == null || arr.length < 2){return;}
for (int len = arr.length - 1; len > 0; len--){ //每次循环确定最后一个值,i的循环最大值前移一位
for (int i = 0; i < len; i++){
if (arr[i] > arr[i + 1]){ //前一个比后一个大,往上冒泡
swap(arr,i,i+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; j--){
if(arr[j] > arr[j + 1]){
swap(arr,j,j+1);
}
}
}
}
3.递归行为
1、时间复杂度分析
master公式:
T(N) = a * T(N/b) + O(Nd)
logba < d O(Nd)
logba > d O(Nlog~b~a)
logba == d O(Nd * logN)
2、寻找最大值
public static int process(int[] arr, int L, int R){
if(L==R){return arr[L];}
int mid = L + ((R-L) >> 1);
//直接计算Mid可以是(L+R)/2,但在极端情况下R会过大导致没法计算,因此可以改写成L + (R-L)/2,然后>>1意为右移一位,等同于除2
int leftMax = process(arr,L,mid);
int rightMax = process(arr,mid + 1,R);
return Math.max(leftMax,rightMax);
}
3、归并排序
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 <= M){
help[i++] = arr[p1] <= arr[p2] ? arr[p1++] : arr[p2++];
}
while(p1 <= M){
help[i++] = arr[p1++];
}
while(p2 <= M){
help[i++] = arr[p2++];
}
for (i = 0; i < help.length; i++){
arr[L + i] = help[i];
}
}
1)求小和问题
求出每个数左边比这个数小的和,全部相加为整个数组的小和
public static void MergeSort(int[] arr,int L, int R){
if(L==R){return 0 ;}
int mid = L + ((R-L)>>1);
return 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;
int res = 0; //记录小和
while(p1 <= M && p2 <= M){
res += arr[p1] < arr[p2] ? (r - p2 +1) * arr[p1] : 0;
help[i++] = arr[p1] <= arr[p2] ? arr[p1++] : arr[p2++];
}
while(p1 <= M){
help[i++] = arr[p1++];
}
while(p2 <= M){
help[i++] = arr[p2++];
}
for (i = 0; i < help.length; i++){
arr[L + i] = help[i];
}
return res; //返回小和
}
4、快速排序
1)荷兰国旗问题
一、给定一个数组arr,和一个数num,请把小于等于num的数放在数组的左边,大于num的数放在数组的右边。额外空间复杂度O(1),时间复杂度O(N)
思路:
变量left代表左边界,i指向当前位置
- [i] <= num,和left+1交换,left++,i++
- [i] > num, i++
public static void Holland(int[] arr,int num){
int left = 0;
for (int i = 0;i < arr.length;i++){
if (arr[i] <= num){
swap(arr,i,left++);
}
}
}
二、给定一个数组arr,和一个数num,请把小于num的数放在数组的左边,大于num的数放在数组的右边,等于num的数放在数组中间。额外空间复杂度O(1),时间复杂度O(N)
思路:
变量left代表左边界,right代表右边界,i指向当前位置
- [i] < num,和left+1交换,left++,i++
- [i] == num,i++
- [i] > num,和right-1交换,right–,i不变
public static void Holland2(int[] arr,int num){
int left = -1;
int right = arr.length;
int i = 0;
while (i < right){
if(arr[i] < num){
swap(arr,i++,++left);
}else if(arr[i] > num){
swap(arr,i,--right);
}else {
i++;
}
}
}
2)快速排序quicksort
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};
}
4.堆排序
1)完全二叉树
从左往右依次填的树就是完全二叉树
父节点 (i - 1) / 2
左子 2 * i + 1
右子 2 * i+2
2)堆排序
每一个子树的头节点是最大值/最小值的称为大/小根堆
插入堆heapInsert,把下面的数往上移
public static void heapInsert(int[] arr, int index){
//1、当比父节点大时就执行交换,小于等于父节点就跳出while
//2、当index=0时,就变成了自己跟自己比,也会停止
while(arr[index] > arr[(index - 1) / 2] ){
swap(arr,index,(index - 1) / 2);
index = (index - 1) / 2;
}
}
堆化数组heapify,把上面的数往下移
public static void heapify(int[] arr, int index, int heapSize){ //heapSize用来记录堆的边界
int left = index * 2 +1; //左子下标
while(left < heapSize){ //先判断下方有无孩子,因为右一定大于左所以只判断左
//两个孩子谁大把下标给largest
//右儿子存在 && 右子比左子大 ? 右子 : 左子
int largest = left + 1 < heapSize && arr[left + 1] > arr[left] ? left + 1 : left;
//父与子直接谁大把下标给largest
largest = arr[largest] > arr[index] ? largest : index;
if(largest == index){ //没比过就跳出了
break;
}
swap(arr, largest, index); //子大那就交换
index = largest;
left = index * 2 + 1;
}
}
堆排序heapSort
- 依次执行heapInsert,完成数组堆化
- 把heapSize位置的数跟0位置交换,heapSize–,此时最大值到最后位置且与heap断连
- 对0位置执行heapify,保持数组为大根堆
- 重复操作2-3,当heapSize==0时,排序完成
public static void heapSort(int[] arr){
if(arr == null || arr.length < 2){
return;
}
for(int i = 0; i < arr.length; i++){ //O(N)
//把每个数依次执行插入堆,完成数组堆化
heapInsert(arr,i); // O(logN)
}
int heapSize = arr.length;
swap(arr,0,--heapSize);
while(heapSize > 0){ //时间复杂度O(N)
heapify(arr, 0, heapSize); //O(logN)
swap(arr, 0 , --heapSize); //O(1)
}
只让数组变成大根堆的方法:
倒过来,从后往前使用heapify
for(int i = arr.length - 1; i >= 0; i--){
heapify(arr, i, arr.length);
}
小根堆(优先级队列)
- 有缺点:中间的数弹出很低效,有需要中间数操作的手写堆,只需要头尾操作的可以用黑盒
PriorityQueue<Integer> heap = new PriorityQueue<>();
while(!heap.isEmpty()){
System.out.println(heap.poll());//每次弹出的就是最大值
}
比较器概念
- 等同于C++重载比较运算符
public static class IdAscendingComparator implements Comparator<Student>{
//返回负数的时候,第一个参数排前面
//返回正数,第二个数排前面
//返回0,无所谓
@Override
pubic int compare(Student 01, Student 02){
return 01.id - o2.id;
}
}
Arrays.sor(students,new IdAscendingComparator())
利用比较器,实现黑盒大根堆排序
public static class Acomp implements Comparator<Integer>{
@opverride
public int compare(Integer arg0, Integer arg1){
return arg1 - arg0;
}
}
PriorityQueue<Integer> heap = new PriorityQueue<>(new Acomp());
while(!heap.isEmpty()){
System.out.println(heap.poll());//每次弹出的就是最小值
}
5.不基于比较的排序
- 必须根据数据状况定制,没那么好用
计数排序
统计词频
基数排序
- 排的数据必须有进制
- 把所有数字的位数统一到跟最多位的数一样,如[2,100] -> [002,100]
- 根据末位数字堆到对应数字的桶里
- 先进先出,把数字倒出来
- 根据倒数第二位数字堆到对应数字桶里
- 先进先出,把数字倒出来
- 重复操作,把所有位排完数组就有序了
public static void radixSort(int[] arr){
if(arr == null || arr.length < 2){
return;
}radixSort(arr,0,arr.length - 1,maxbits(arr));
}
public static int maxbits(int[] arr){
int max = Integer.MIN_Value;
for(int i = 0; i < arr.length; i++){
max = Math.max(max,arr[i]);
}
int res = 0;
while(max != 0){
res++;
max /= 10;
}
return res;
}
public static void radixSort(int[] arr, int L, int R, int digit){
final int radix = 10; //以10为基底,即所有数都是10进制
int i = 0, j = 0;
//有多少个数准备多少个辅助空间
int[] bucket = new int[R - L + 1];
for(int d = 1; d <= digit; d++){ //有多少位就进出多少次
//10个空间
//count[0]当前位(d位)是0的数字有多少个
//count[1]当前位(d位)是(0和1)的数字有多少个
//count[2]当前位(d位)是(0、1和2)的数字有多少个
//count[i]当前位(d位)是(o~i)的数字有多少个
//相当于先每个位置词频统计,再在每一位上把把前面的从词频累加
//于是开始从后往前遍历数组,count里的数是多少,就说明在这个数前面有几个数,count里的这个值就是数应该在桶里的位置
int[] count = new int[radix]; //count[0..9]
//词频统计
for(i = L; i <= R; i++){
j = getDigit(arr[i], d);
count[j]++;
}
//累加
for(i = 1; i < radix; i++){
count[i] = count[i] + count[i - 1];
}
//进桶,根据对应位置count得出该在的位置,进桶后该位置count--
for(i = R; i >= L; i--){
j = getDigit(arr[i], d);
bucket[count[j] - 1] = arr[i];
count[j]--;
}
for(i = L, j = 0; i <= R; i++, j++){
arr[i] = bucket[j];
}
}
}
//提取出指定位置的数字
public static int getDigit(int x, int d){
return ((x / ((int) Math.pow(10, d - 1))) % 10);
}
排序算法 | 时间复杂度 | 空间复杂度 | 稳定性 |
---|---|---|---|
选择 | O(n2) | o(1) | × |
冒泡 | O(N2) | O(1) | √ |
插入 | O(N2) | O(1) | √ |
归并 | O(N*logN) | O(N) | √ |
快排(随机p) | O(N*lognN) | O(logN) | × |
堆 | O(N*logN) | O(1) | × |
总结:能用快排用快排,空间不够用堆排,需要稳定性用归并
基于比较的排序:
- 时间复杂度小于O(N*logN)不存在
- 在时间复杂度在O(N*logN)的情况下,空间复杂度在O(N)以下且保持稳定性不存在