快速排序
快速排序是一种就地排序,会改变输入数组,同时快速排序是一种不稳定的排序。快速排序的关键点是选择一个合适的轴点(pivot
),这个选取的不确定性也导致了每次数据的分割是不均匀的。
对于选取的一个轴点,其左边的都不大于轴点,右边的都不小于轴点: [low, pivot-1] <= pivot <= [pivot+1, high]
int partation(std::vector<int> &arr,int left,int right){
//第一步,随机选取一个数作为pivot,这里我们选择第一个数即可
int p = left;
std::swap(arr[p],arr[right]); //交换arr[p],arr[right],此时arr[right]等于我们选择的pivot
int i=left;
for(int j=left;j<right;++j){
if(arr[j]<=arr[right]){
std::swap(arr[i],arr[j]);
++i;
}
//定义i,j=left,让j进行循环,如果啊arr[j]<=arr[r],说明当前数字应该在pivot的左边,所以交换i和j,并i++,当循环完这个操作,i,j不用再交换意味着在left到i这个区间全部小于pivot,i+1到right-1,这个区间全是大于pivot,此时我们将arr[i]和arr[r](即最初的pivot)交换,即成功的将其进行了划分,并且i就是我们的pivot的位置
}
std::swap(arr[i],arr[right]);
return i;
}
void quickSort(std::vector<int> &arr,int left,int right){
//结束条件,当数组中只有一个数自然是有序的,即right<=left
if(right<=left)
return;
int p = partation(arr,left,right);
quickSort(arr,left,p-1);
quickSort(arr,p+1,right);
}
画个草图示意一下划分过程
partition
算法,就是在扫描数据的过程中,将所有大于轴点的数据放到轴点右边,将所有小于轴点的数据放到左边。而quickSort
算法就是不断的分割,当只有一个数据时,其本身就是有序,再将所有的子问题组合起来,思想是“分而治之”。
复杂度
可在线性时间内将原向量排序问题划分为两个相互独立、总体规模保持线性的子向量排序问题。但是分治策略高效的两个必要条件:
- 子任务划分的高效性及子任务之间的独立性:满足
- 子任务规模的接近:不满足。
实际上,不仅仅不满足条件 2,还是有可能相差悬殊。这是因为划分所得子序列的长度与划分的具体过程无关,而是取决于轴点。比如轴点 rank=r ,那么子向量的规模是 r
个 n-1-r
。当r=0,左侧子向量规模为空,右侧与原来的向量等长,对称即 r=n-1
。这对于有序向量而言,每次都是简单的选取最左侧元素作为轴点,此时的效率是 O(n^2)。
降低最坏概率情况
不能每次固定的选取最左端元素,可以每次自由随机选取一个元素
平均运行时间
最差的时间复杂度是 O(n^2),但在平均意义下,时间复杂度是 O(1.38logn)
应对退化情况
当所有的元素都是重复的时候,此时对应的情况即之前的最坏情况.因为循环中的 pivot <= arr[right]
会一直满足,直到 low < high
不满足,最后整个算法退化为线性递归,时间复杂度是o(n^2)
。
改进如下:
int partation(std::vector<int>& arr, int L, int R) {
int pivot = arr[L];
while(L < R) {
while(L < R && pivot <= arr[R]) --R; if(L < R) arr[L++] = arr[R];
while(L < R && arr[L] <= pivot ) ++L; if(L < R) arr[R--] = arr[L];
}
arr[L] = pivot;
return L;
}
如此,每次遇到重复的元素,都是将元素放到的对立的区间,当循环中止的时候,恰好位于中间的位置,此时的时间复杂度最好,是O(nlogn)
继续优化
我们知道,在序列基本有序的情况下快速排序的时间复杂度会退化到O(n^2),原因就是轴点的选取的不当导致一共需要n层,每层的时间复杂度为O(n),所以此时我们就优化基本有序的情况就是选择一个随机数做为轴点,以下
int partation(vector<int> &arr,int l,int r){
int p = rand()%(r-l+1)+l;
swap(arr[p],arr[r]);
int i=l,j=l;
for(;j<r;j++){
if(arr[j]<arr[r]) swap(arr[i++],arr[j]);
}
swap(arr[i],arr[r]);
return i;
}
暂时更到这
mergeSort
归并排序分为两个部分,先将整个序列化为2个规模一样的序列,然后排序。递归的调用,不断的划分,直到子序列的规模为1,那么这个规模为1的子序列就是有序的(是不是与快排的思想类似)
void mergeSort(vector<int> &arr,int lo,int mid,int hi){
if(hi-lo<2) return;
int mid = (lo+hi)>>1;
mergeSort(arr,lo,mid);
mergeSort(arr,mid,hi);
merge(arr,lo,mid,hi);
}
- 递归基:当hi-lo==1时,就是只有一个元素,此时有序,返回
- 中点:每次都是取中点,将数据区间划分为两个等规模区间
- 归并:当划分到单独元素时,开始合并数据
#define inf 0x3fffff
void merge(vector<int> &arr,int lo,int mid,int hi) {
//复制左边半部分
vector<int> l(arr.begin()+lo,arr.begin()+mid);
l.push_back(inf);
//复制右边部分,注意一下这种复制是左闭右开,所以是arr首地址+hi+1
vector<int> r(arr.begin()+mid,arr.begin()+hi+1);
r.push_back(inf);
int i=0,j=0,k=lo;
while(k<=hi) {
if(l[i]<=r[j]) arr[k++]=l[i++];
else arr[k++]=r[j++];
}
}
归并排序我看别人写的merge部分都比较臃肿,判断条件太多,因此我进行了一点优化,即在左右两个子序列引入一个无穷大的哨兵,这样就不用判断i,j是否超出范围
冒泡排序
冒泡排序是排序中得基本算法,时间复杂度O(n^2)
消除逆序对
冒泡法,每一趟遍历都会消除一些逆序对,让最大值元素归位。比如第一趟遍历让最大值元素归位,第二趟让第二大元素归位,一直到第n趟让第n大,也就是最小的元素归位。
void bubbleSort(std::vector<int> &arr){
if(arr.empty) return ;
int r = arr.size()-1;
for(int l=0,l<r--;l++)//r--的意思就是最大值已经沉底,不用再去比较,能省一点时间
for(int l=0;l<r;l++){
if(arr[l]>arr[l+1])
std::swap(arr[l],arrr[l+1]);
}
}
这其实也是一种贪心的思想体现:因为只要将遇到的所有“逆序对”消除,那么就是一个有序的序列,而冒泡排序就是每遇到一个逆序对就修正这个逆序对,消除他,通过n趟的迭代,那么就能消除所有的逆序对,就会变得有序。
时间复杂度
毫无疑问是O(n2)。即使是一个有序的序列,用冒泡法时间复杂度依然是O(n2)。因为在每一轮迭代中,只是考虑逆序对,将最大未归位元素归位。可是如果前面的部分元素已经归位,那么是否可以直接跳过而不再迭代???
void bubbleSort(std::vector<int> &arr){
if(arr.empty) return ;
int r = arr.size()-1;
for(int l=0,l<r--;l++)//r--的意思就是最大值已经沉底,不用再去比较,能省一点时间
{int last = 0;
for(int l=0;l<r;l++){
if(arr[l]>arr[l+1]){
std::swap(arr[l],arrr[l+1]);
last=l;//记录最后一次交换元素的位置
}
}
r=last;//下次直接从最后一次更新元素位置开始
}
}
如此不仅在序列本身有序时时间复杂度仅为O(n),也为普通情况下进行了改善,不再是每次都遍历,而是直接跳到最后一次交换元素的位置。