一、异或运算的使用
1.交换两个元素位置
- a=a^b;
- b=a^b;
- a=a^b;
2.在一串数中,找到只出现奇数次的数(只有一个数出现奇数次,其余数都出现偶数次)
进阶:找到两个出现奇数次的数(只有两个数出现奇数次,其余数都出现偶数次)
二、二分法的详情与拓展
1.在有序数组中,找到某个数是否存在
2.在有序数组中,找>=某个数最左侧的位置,或找<=某个数最右侧的位置
3.局部最小值问题:在强无序(任何两个相邻的数一定不相等)数组中找到一个局部最小值
三,master公式(计算递归的时间复杂度)
T(N)=a*T(N/b)+O(N^d)
1).log(b,a)>d => 时间复杂度为O(N^log(a,b))
2).log(b,a)=d => 时间复杂度为O(N^d*logN)
3).log(b,a)<d =>时间复杂度为O(N^d)
四,归并排序(merge)...分治思想
1.整体是一个简单递归,左边排好序,右边排好序,让其整体有序
2.让其整体有序的过程里用了外排序的方法
3.利用master公式求时间复杂度O(n*logn)空间复杂度为O(n)
4.归并排序的实质
5.归并排序的拓展(小和问题,逆序对问题)
五,快速排序(quickSort)空间复杂度O(n)
1.荷兰国旗问题
- 给定一个整数数组和一个值M(存在于原数组中),要求把数组中小于M的元素放到数组的左边,等于M的元素放到数组的中间,大于M的元素放到数组的右边,最终返回一个整数数组,只有两个值,0位置是等于M的数组部分的左下标值、1位置是等于M的数组部分的右下标值
2.快速排序的三个版本
- 快排1.0:一次遍历确定一个元素的位置,时间复杂度O(n^2)...一般以最后一个元素为基准元素
- 快排2.0:一次遍历确定一批元素位置,实际时间复杂度稍好,最差时间复杂度仍为O(n^2)...一般以最后一个元素为基准元素
- 快排3.0:快排2.0的优化,快排2.0最差时间复杂度为O(n^2),是因为总能举出最差的例子,如顺序序列或逆序序列,每次确定一个位置的元素时,该元素不能将整个序列划分为平均两部分,这样快排与冒泡排序和选择排序无异,但如果将选取基准元素设为随机(使用rand()函数),则时间复杂度是各种情况概率累加的数学期望,为O(nlogn)
六,堆排序(heapSort) 空间复杂度O(1)
1.堆
- 堆结构就是使用数组实现的完全二叉树结构,该二叉树中,每颗子树的最大值都在顶部则是大根堆,反之是小根堆 //下面说法都是基于大根堆
2.堆的基础操作
- heapInsert,实质是向上找,具体操作为若该元素大于父元素,则该元素与父元素互换,重复该操作一直向上找,直到找到该元素所在位置即该元素小于其父元素或者该元素成为根结点。heapify,实质是向下找,具体操作为若该元素小于孩子元素,则该元素与子元素中最大的互换,重复该操作一直向下找,直到找到该元素所在位置即该元素大于其子元素或者该元素成为叶结点
3.堆排序
- 基于上面两基础操作,首先使用上面两个基础操作heapInsert(时间复杂度为O(nlogn))或heapify(时间复杂度为O(n))将数组调整为大根堆,然后将最后一个元素与根结点互换,堆的长度heapSize-1,再使用heapify(因为最后最后一个元素一定小于根元素也就是堆中最大的元素,互换后根元素不是最大值了,所以需要使用heapify将当前根元素向下调整,直到成为大根堆)将堆调整为大根堆,重复上述操作直到heapSize为0。时间复杂度为O(nlogn),故总时间复杂度为O(nlogn)
七,比较器的使用
- 比较器的实质就是重载比较运算符
- 比较器可以很好的运用在特殊标准的排序上
- 比较器可以很好的运用在根据特殊标准排序的结构上
- 比较器默认返回负数的时候第一个参数放到前面,返回正数的时候第二个参数放到前面,返回0的时候谁在前面无所谓。
int compare(ElemType a1,ElemType a2){ return a2-a1; }//按逆序排列,如a2>a1则a2放在前面
八,基数排序 (radixSort)或桶排序(buckerSort)
1.基数排序的特点
- 冒泡排序,选择排序,插入排序,归并排序,快速排序,堆排序都是基于元素相互比较进行排序 ,而基数排序不需要通过比较就可以进行排序,且时间复杂度低O(n)
2.代码实现:此代码经过优化
void Radixsort(int *arr,int L,int R){
int digit=0;//表示数组中最大的数的位数
Maxbits(arr,L,R,digit);
int bucker[R-L+1];//有多少个数就设置几个空间
for(int i=1;i<=digit;i++){
//排序次数与最大值位数有关
int count[radix]={0};
for(int j=L;j<=R;j++){
int num;
bitnum(arr[j],num,i);
count[num]++;
}
for(int j=1;j<radix;j++){
//找到小于等于当前位的值的个数
count[j]=count[j-1]+count[j];
}
for(int j=R;j>=L;j--){
//必须从后往前遍历,保证当前位的值相同时,可以按低一位值大小排序
int k;
bitnum(arr[j],k,i);
bucker[count[k]-1]=arr[j];
count[k]--;
}
for(int j=L;j<=R;j++){
arr[j]=bucker[j];
}
}
}
void bitnum(int arr,int &num,int digit){
//找到元素digit位的值
num=arr/pow(10,digit-1);
num=num%10;
}
void Maxbits(int *arr,int L,int R,int &digit){
//找到数组中最大的数的位数
int max;
for(int i=L;i<=R;i++){
max=max>=arr[i] ? max:arr[i];
}
while(max){
digit++;
max=max/10;
}
}
九,排序内容总结
1.各种排序的优缺点
时间复杂度 | 空间复杂度 | 稳定性 | |
选择排序 | O(N^2) | O(1) | 不稳定 |
冒泡排序 | O(N^2) | O(1) | 稳定 |
插入排序 | O(N^2) | O(1) | 稳定 |
归并排序 | O(N*logN) | O(N) | 稳定 |
快速排序 | O(N*logN) | O(logN) | 不稳定 |
堆排序 | O(N*logN) | O(1) | 不稳定 |
2.常见的坑
-
归并排序的空间复杂度可以变为O(1),但是非常男,不需要掌握,有新兴趣可以搜"归并排序内部缓存法"
-
原地归并排序会让归并排序的时间复杂度变为O(N^2)
-
快速排序可以做到稳定性问题,但非常难,有兴趣可以搜"01 stable sort"
-
所有的排序算法优化都基于牺牲三者中的一个或两个而去优化另一个,目前还没有找到时间复杂度为O(N*logN),空间复杂度为O(1),又稳定的排序算法