排序的分类
插入排序
在一组数据中,把无序数据一个一个插入到有序序列中(刚开始的时候默认无序数列中第一个元素为有序数列),并每次插入时通过把元素不断前后搬移使之排成有序序列。不断插入无序数列的数据到有序数列,最终无序序列成为有序序列。如果序列是目标有序的数组(即如果我们想把它升序,数组是升序数组,降序,数组是降序数组)这样的情况下,插入排序是所有排序中最快的,它的时间复杂度为O(N) , 只遍历一遍数组就结束。
希尔排序
希尔排序是在插入排序中进行了改进,每次排序时都定义了个gap(它为分组中元素的偏移量)。每次进行排序时,将无序数列分为多个分组,将每一组的元素进行插入排序。每次gap值都递减,最终递减至1时,将整个数据进行一次插入排序。
但是如果数组是有序的话,希尔排序的效率就不那么好,不如插入排序好,因为特殊为插入排序改进的gap处理是无用功,所以效率就不是那么理想了。
插入与希尔
时间复杂度 O(N^2)
空间复杂度 O 1
适用场景 接近有序数据量少(这条只针对插入)
希尔排序原理图
希尔排序效果图
代码实现
直接插入
void Insert_Sort(int * array, size_t size)
{
for (size_t idx = 1; idx < size; idx++)
{
int end = idx-1;
int key = array[idx];
while (end>=0 && array[end] > key)
{
array[end + 1] = array[end];
end--;
}
array[end + 1] = key;
}
}
优化版插入排序它找待插位比较的次数比上面的代码少
void Insert_Sort_(int array[] , size_t size)
{
for (size_t idx = 1; idx < size; idx++)
{
int left = 0;
int right = idx - 1;
int key = array[idx];
int end = idx - 1;
while (left <= right) // 二分查找 ,找恰当的位置,这个位置刚好 ,前一个比key小后一个比key大
{
int mid = (left + right) >> 1;
if (key>=array[mid]) left =mid+1; //因为插入排序是稳定的,所以当key等于有序数列的某个元素时,也要在后半段插入才符合插入排序的性质
else right = mid - 1;
}
while (end >= left) //找到该位置进行搬移, left到这有俩种情况。情况一有序数列所有元素都小于等于key,则最后次left与right都指向了有序数列的最后一个元素 所以mid也指向了它。left+1那个位置就是key的位置,所以进不来while
{ //第二种情况 就是left在有序数列里,则从left开始向后搬移
array[end + 1] = array[end];
end--;
}
array[left] = key; //这里key跟while一样分俩种,第一种就是无序数列待插元素的位置,第二种在序列里找到的待插位的位置
}
}
希尔排序代码
void shell_sort(int * array, size_t size)
{
int gap = size;
while (gap > 1)
{
gap = gap / 3 + 1; // 它指分组中每个元素的偏移量,也指无序数列有几个分组。当gap为1代表每个元素偏移量为1,所以组内元素都是连续的中间没有被分隔开。只有1组所以代表整个数组。
for (size_t idx = gap; idx < size; idx++)
{
int end = idx - gap;
int key = array[idx];
while (end>=0&&array[end]>key)
{
array[end + gap] = array[end];
end -= gap;
}
array[end + gap] = key;
}
}
}
shell排序:while层控制每次分组的gap值大小,内层负责每次插入排序。内部的for用来控
制插入排序的次数,for里面的while负责真正意义上的交换。
选择排序
选择排序
它的原理,如果是升序的情况,在一组无序数列中选取最大的一位与无序数列最后一位交换,然后继续从剩下的无序序列中再次选取一位最大的数再与剩下的无序序列中的最后一位交换,一直不断循环至无序数列只剩下一位数字,那么这个数列中其他数就是有序的了。(降序思路一样,但是它是选最小的数字)
选择排序代码
void select_soct(int *array, size_t size)
{
size_t end = 0;
size_t maxpos = 0;
for (size_t idx = 0; idx < size - 1; idx++)
{
maxpos = 0; //这个maxpos =0 这个是必须的,因为maxpos为无序序列中最大的位,所以当最后只剩下0号与1号时
//逻辑也是正确的。但是如果不重置POS的话,可能发生那maxpos指向有序数列这种情况,但是max应指向无序中最大的一个。
for (size_t j = 0; j < size - idx; j++)
{
if (array[j]>array[maxpos]) {
maxpos = j;
}
}
end = size - idx - 1;
if (maxpos != end)
{
array[maxpos] ^= array[end];
array[end] ^= array[maxpos];
array[maxpos] ^= array[end];
}
}
}
void SelectSort(int*arr,size_t N)
{
int max=0;
int min=0;
int left=0;
int right=N-1;
while(left<right)
{
max=right;
min=left;
for(int i=left;i<=right;++i)
{
if(arr[i]>arr[max])
max=i;
if(arr[i]<arr[min])
min=i;
}
std::swap(arr[min],arr[left]);
if(left==max)
max=min;
std::swap(arr[max],arr[right]);
++left,--right;
}
}
效果图
堆排序
堆排原理就是把无序数组建堆,然后用堆顶元素和堆最后一个元素交换。交换完毕后,堆元素个数减一,减完后再把之前的堆进行一次调整。(所以升序应用最大堆,降序应用最小堆)
时间复杂度
O(nlog2n)
这里每次调整是log2n,调整的次数是整个数组次数,所以排序的时间复杂度是O(nlog2n),建堆O(nlog2n),加起来后时间复杂度O(nlog2n)。
空间复杂度
O(1)
这里开辟的变量是常数的所以是O(1)。
代码
void HeapAdjust(int*array, int root, int size)
{
size_t parent = root;
size_t child = 2 * root + 1;
while (child < size)
{
if ((child+1<size) && (array[child] < array[child + 1]))
{
child = child + 1;
}
if (array[parent] < array[child])
{
array[parent] ^= array[child];
array[child] ^= array[parent];
array[parent] ^= array[child];
}
else
{
return;
}
parent = child;
child = 2 * parent + 1;
}
}
void Head_select(int * array, int size)
{
int last = ((size - 1) - 1) / 2;//最后一个非叶结点,这里为int挺重要的,因为最后一次对整个完全2叉树进行调整
for (int idx = last; idx >= 0; idx--)//这里最后一次是root为0号元素,调整完后无符号0再减一发生溢出就死循环了
{
HeapAdjust(array, idx, size); //建堆
}
int end = size - 1;
for (int idx = end; idx > 0; idx--)
{
array[idx] ^= array[0];
array[0] ^= array[idx];
array[idx] ^= array[0];
HeapAdjust(array, 0, idx);
}
}
效果图
交换排序
冒泡排序
冒泡排序原理就是从前到后一次俩俩遍历,如果符合条件就交换,每次遍历完一次有个无序数组个数减一位,所以每次遍历完后无序数组最后一位都是该无序数组中最大或最小的一位。具体写法俩层循环,外层控制次数内层控制具体的交换。
时间复杂度 O(n2) —–>n平方
空间复杂度 O (1)
void Mubble_selcet(int * array, int size)
{
for (size_t i = 0; i < size - 1; i++)
{
for (size_t j = 0; j < size - i-1; j++) //这个减一蛮重要的,如果你是用j来比的,这里必须要减1,防止越界
{
if (array[j]>array[j + 1])
{
array[j] ^= array[j + 1];
array[j+1] ^= array[j];
array[j] ^= array[j + 1];
}
}
}
}
效果图
快速排序
快速排序的原理是选择一个基准值,如果是升序的情况下,基准值左边比基准值小,基准值右边比基准值大。然后在基准值的左半区间取左半区间基准值,再右半区间取右半基准值。然后左右区间分别再次进行排序。最终不断递归,无序数列成为有序数列。所以把快速排序还可以与插入排序组合到一起来优化,使之降低递归层数从而到达优化。
适用场景
越无序越高效,最低效的情况是每次取基准值都是最大值或者最小值,这样的情况下时间复杂度降到O(n2)
时间复杂度和空间复杂度
时间复杂度O(nlog2n)
空间复杂度O(1) (这里指的是额外的空间复杂度)
代码
partion : 为horn法,单次遍历时有俩个指针遍历无序数组,begin往后遍历直到找到比基准值大的值停下,end往前遍历直到找到比基准值小的值停下。然后俩个指针指向的内容交换,直到begin与end相遇时结束该次遍历。然后不断递归该遍历的算法,不断的以基准值的左区间进行遍历,右区间遍历最终为有序数列。
int partion(int * array, int left, int right)
{
int mid = GetmidIndex(array, left, right);
if (mid != right) swap(array[left], array[right]);
int &key = array[right];
int end = right;
int begin = left;
while (begin<end)
{
while (begin<end&&array[begin] <= key)
begin++;
while (begin < end&&array[end] >= key)
end--;
if (begin < end)
{
swap(array[begin], array[end]);
}
}
if (begin != right)
{
Swap(array[begin], key);
}
return begin;
}
partion2:挖坑法,基准值...
int partion2(int * array, int left, int right)
{
int mid = GetmidIndex(array, left, right);
if (mid != right) swap(array[left], array[right]);
int key = array[right];
int end = right;
int begin = left;
while (begin < end)
{
while (begin < end&&array[begin] <= key)
begin++;
array[end--] = array[begin];//每次遍历必会发生begin与end相遇,这里给end时无错,因为begin没有想后走。
while (begin < end&&array[end] >= key)//但这里给begin赋值时,如果不添加if判断,最后自己给自
end--;//己赋值的时候,begin会向后走一步,后面在用key填坑时会出错
if(begin<end) array[begin++] = array[end];
}
array[begin] = key;
return begin;
}
/////////////挖坑法非递归版/////////////
void My_sort_nor(int *array, int left, int right)
{
assert(array != 0 && left < right);
stack<int> scon;
scon.push(left);
scon.push(right);
while (!scon.empty())
{
int end = scon.top();
scon.pop();
int begin = scon.top();
scon.pop();
int right = end;
int left = begin;
if (begin < end)
{
int key = array[end];
while (begin < end)
{
while (begin < end&&array[begin] <= key)
++begin;
if (begin < end)
array[end--] = array[begin];
while (begin < end&&array[end] >= key)
--end;
if (begin < end)
array[begin++] = array[end];
}
array[begin] = key;
int Base = begin;
scon.push(Base + 1); // 入栈的区间 和出栈的区间是顺序是相反的这个需要注意下
scon.push(right);
scon.push(left);
scon.push(Base - 1);
}
}
}
前后指针法思想
前后指针法,分俩个阶段,第一阶段找第一个比基准值大的阶段,在这个阶段中,prev 与 pCur 同步走,当出现第一个大于基准值的元素时pCur走到该元素处,prev 此时位置在该元素前面一个位置。
现在进入第二阶段,pCur向后走,每遇见一个小于基准值的元素就让prev先走一步,然后与之交换。
好了这个思想最关键的就是第二阶段,只要进入第二阶段了,pCur和prev之间位置至少差距为2,因为刚进入的时候,pCur走到了临界元素的后面一个位置,prev在临界元素前面的一个位置,之后当遇见小于基准值的元素prev才走一步,所以至少是大于2步的它们之间间隔,所以最差就是这种情况
1 9 2 3 4 5 6 7 8
| |
prev pCur
一直用这个9往后交换,不过也没什么,结果是正确的。
/********************
****前后指针法*****
*******************
*******************/
int partion3(int * arr, int left ,int right)
{
int pCur = left;
int prev = left-1;
int key = arr[right];
while(pCur<=right)
{
if(arr[pCur]<=key&&prev++!=pCur)
std::swap(arr[prev],arr[pCur]);
++pCur;
}
return prev;
}
void _qsort(int * arr, int left ,int right)
{
if(left>=right)
return ;
int base = partion(arr,left,right);
_qsort(arr,left,base-1);
_qsort(arr,base+1,right);
}
void qsort(int * arr,size_t N)
{
_qsort(arr,0,N-1);
}
void printarr(int * arr, size_t N)
{
for(int i = 0 ; i < N ; ++i)
{
cout<<arr[i]<<" ";
}
cout<<endl;
}
int main()
{
int n = 10;
int arr[n] = {7,0,9,8,6,4,5,3,1,2};
qsort(arr,n);
printarr(arr,n);
return 0;
}
效果图
归并排序
时间复杂度/额外空间复杂度
时间复杂度 o(log^2n)
额外空间复杂度 O(N)
思路
每次把区间分为俩半,每一半都交给递归继续去分裂与排序,等俩个子区间递归结束后,那肯定都是有序的,再借助辅助区间,将俩个子区间合并后赋值会原区间即可。
每次排序子区间的时候,必须把结果回写到目标数组中,因为辅助数组只是辅助合并有序元素的,而真正的数组如果不写回,元素间还是无序的,因为上一层的递归合并的时候依赖下一层合并的结果,如果不写会原数组的话,还是没用,所以在每一次调用的递归函数内都必须得写会数据。
#include<iostream>
#include<string.h>
using namespace std;
void _MergeSort(int*arr,int left ,int right,int * tmp)
{
if(left<right)
{
int mid = left + (right-left>>1);
_MergeSort(arr,left,mid,tmp);// 明显交给递归去分裂区间并排序
_MergeSort(arr,mid+1,right,tmp);//明显交给递归去分裂区间并排序
int begin = left ;
int end = mid;
int begin2 = mid+1;
int end2 = right;
int index = left;
while(begin<=end&&begin2<=end2)
{
if(arr[begin]<arr[begin2])
tmp[index++]=arr[begin++];
else
tmp[index++]=arr[begin2++];
}//先出来循环那个区间的end肯定大于begin
while(begin<=end)
tmp[index++]=arr[begin++];
while(begin2<=end2)
tmp[index++]=arr[begin2++];
memmove(arr+left,tmp+left,sizeof(int)*(right-left+1));
}
}
void MergeSort(int * arr, size_t N)
{
int * tmp = new int [N];
_MergeSort(arr,0,N-1,tmp);
delete [] tmp;
}
void PrintArr(int * arr ,size_t N)
{
for(int i=0;i<N;++i)
{
cout<<arr[i]<<" ";
}
cout<<endl;
}
int main()
{
int arr[10] = {8,1,4,0,3,9,2,6,5,7};
MergeSort(arr,10);
PrintArr(arr, 10);
return 0;
}
计数排序
思想
思想就是开一个哈希表,统计次数,然后再写会。
时间复杂度 O(N+M) ,空间复杂度 O(M) , M 为 max -min +1 所开辟的辅助空间大小
#include<iostream>
#define Length 10
using namespace std;
void CountSort(int * arr,size_t N)
{
int max=arr[0];
int min=arr[0];
for(int i=0;i<N;++i)
{
if(arr[i]>max)
max = arr[i];
if(arr[i]<min)
min = arr[i];
}
int range = max - min + 1;
int * tmp = new int [range];
for(int i = 0; i < N ; ++i)
{
++tmp[arr[i]-min]; // 统计次数
}
int index = 0;
for(int i = 0; i < range ; ++i)
{
while(tmp[i]--)
arr[index++]=i+min; //写会
}
delete tmp;
}
void printarr(int * arr, size_t N)
{
for(int i=0; i < N ;++i)
{
cout<<arr[i]<<" ";
}
cout<<endl;
}
int main()
{
int arr[Length] = {1,6,0,9,7,5,2,3,4,8};
CountSort(arr,Length);
printarr(arr,Length);
return 0;
}