最近复习了排序算法,所以在这篇博客里总结一下。
这里的八大排序有:冒泡排序、快速排序、直接插入排序、shell排序、选择排序、堆排、归并排序和基数排序。
因为基数排序没看,所以下面只会出现前面7种排序,之后可能会加上把。。。
首先简单分个类
- 交换:冒泡排序、快速排序
- 插入:直接插入排序、shell排序
- 选择:选择排序、堆排
- 分配:归并排序、基数排序
下面就一个一个来看把。
一、冒泡排序
算法:从数组末尾开始,两个两个比较,小的交换到前面,大的交换到后面,就像泡泡一样,小的数慢慢交换到最前面
时间复杂度:平均 :O(N^2) 最好:O(N) 最坏:O(N^2)
空间复杂度:O(1)
稳定性:稳定
代码:
void bubble_sort(int arr[],int low,int high)
{
bool flg = true;//记录一趟中是否进行了交换
for (int i = low; i < high&&flg; ++i)//趟数
{
flg = false;//默认没有交换
for (int j = high; j > i; --j)//一趟遍历
{
if (arr[j - 1] > arr[j])//前面的数大,则交换
{
swap(arr[j], arr[j - 1]);
flg = true;//交换了,标志位置为true
}
}
}
}
两层循环,外循环为一共的趟数,因为一趟遍历下来会有一个最小的数排序完成,一共个n个数,只剩下最后一个数的时候不用再走一趟了,所以是n-1趟。内循环为一趟遍历,从最后两个开始遍历,两个数中小的放前面,大的放后面,及前面的数大则进行交换。代码中的flg目的是记录一趟遍历中有没有交换,如果没有交换,那么整个排序肯定也完成了,此时不用继续循环了。
二、快速排序
算法:让要排序数的左边的数都小于它,右边的数都大于它,递归这个过程,使得整个数组有序。
时间复杂度:平均:O(N*log2N) 最好:O(N*log2N) 最差:O(N^2)
空间复杂度:O(log2n)~O(n)
稳定性:不稳定
代码:
void insert_sort(int arr[], int low, int high)//插入排序
{
for (int i = low + 1; i <= high; ++i) //将arr[i]插入有序[low..i-1]部分
{
int sentry = arr[i];
int j;
for (j = i - 1; j >= low && sentry < arr[j]; --j)
arr[j + 1] = arr[j];
arr[j + 1] = sentry;
}
}
int partition(int arr[], int low, int high)
{
int pivotKey = arr[low]; //用子序列的首个元素作为枢轴元素。
while (low < high) //从数组的两端交替向间扫描,只有1个元素需要调整吗?不需要。
{
while (low < high && arr[high] >= pivotKey) --high; //从高端向低端扫描。
arr[low] = arr[high]; //较小值交换到低端。
while (low < high && arr[low] <= pivotKey) ++low; //从低端向高端扫描。
arr[high] = arr[low]; //较大值交换到高端。
}
arr[low] = pivotKey; //枢轴元素归位。
return low;
}
void quick_sort(int arr[], int low, int high)
{
//只有序列中元素个数超过10,才需要进行递归调用。
if (high - low > 10)
{
while (low < high)
{
int pivotLoc = partition(arr, low, high); //找到划分位置。
quick_sort(arr, low, pivotLoc - 1);
low = pivotLoc + 1; //消除尾递归。
}
}
else
insert_sort(arr, low, high);
}
在元素较少的时候用递归不适合,所以用了插入排序,插入排序下面还会提到。快排有两部分组成,一个主体函数和一个分割函数,分割函数的任务:使待排序数左边数都小于它,右边数都大于它(实际上就是确定了这个数的位置),主体函数在对这个数的左右两边再次分割排序(也就是调用递归),最后整个数组便都有序了。
三、直接插入排序
算法:将待排序数插入到已经有序的数组中。
例如:刚开始时,第一个元素可视为有序的,那么将第二个数插入它,就能得到一个有两个有序数的数组,再把第三个数插入这个有两个有序数的数组……以此类推完成排序。
时间复杂度:平均:O(N^2) 最好:O(N) 最坏:O(N^2)
空间复杂度:O(1)
稳定性:稳定
代码:
void insert_sort(int arr[],int low,int high)
{
for (int i = low + 1; i < high + 1; ++i)
{
int temp = arr[i];//待排序数
int j = i - 1;
for (; j >= low && temp < arr[j]; --j)//再有序数组中找插入位置
{
arr[j + 1] = arr[j];
}
arr[j + 1] = temp;//找到位置后插入
}
}
总的过程就是从前往后排序,先把前面的排成有序数组,再把无序的数和有序数组比较,找到一个位置插入。
四、shell排序
算法:插入排序的优化,再插入排序的基础上加入了分组的概念,减少了待排序元素的个数
时间复杂度:平均:O(N^1.3) 最好:O(N) 最坏:O(N^2)
空间复杂度:O(1)
稳定性:不稳定
代码:
void shell_sort(int arr[], int low, int high)
{
int increase = high - low + 1; //增量初始值为序列[low..high]元素个数。
do
{
increase = increase / 3 + 1;
for (int i = increase; i <= high; ++i)将arr[i]插入有序子序列中。
{
int temp = arr[i];
int j = i - increase;
for (; j >= low && temp < arr[j]; j -= increase)//在有序子序列范围内,元素后移寻找插入位置。
{
arr[j + increase] = arr[j];
}
arr[j + increase] = temp;
}
} while (increase > 1);
}
五、选择排序
算法:把数组中最小的选出来,放到最前面,循环这个过程
时间复杂度:平均:O(N^2) 最好:O(N) 最坏:O(N^2)
空间复杂度:O(1)
稳定性:不稳定
代码:
void select_sort(int arr[], int low, int high)
{
for (int i = low; i < high; ++i)
{
int min = i;//记录最小值下标
for (int j = i + 1; j <= high; ++j)
{
if (arr[j] < arr[min])//找到最小值
{
min = j;//更新当前最小值
}
}
if (min != i)//把最小值往前提
{
swap(arr[i], arr[min]);
}
}
}
过程比较简单,用下标记录最小值的位置,遍历数组,找到最小的值,交换到前面,排完就是从小到大的一个数组。
六、堆排
算法:堆排序是排序算法中比较难的一个,需要进行模块化降低难度。抽象出3个动作:1)堆调整;2)构建最大堆;
3)堆排序。初始构建堆,在重建堆的反复筛选。
时间复杂度:平均:O(N*log2N) 最好:O(N*log2N) 最坏:O(N*log2N)
空间复杂度:O(1)
稳定性:不稳定
//[low+1..high]符合最大堆,需要调整使得[low..high]符合最大堆。
void heap_adjust(int arr[], int low, int high)
{
int sentry = arr[low];
int parent = low; //parent指针始终指向当前双亲。
int child = parent * 2 + 1; //child指针始终指向当前孩子。
while(child <= high)
{
if(child < high && arr[child] < arr[child+1]) ++child; //找到较大的孩子。
if(sentry > arr[child]) break; //当前parent指针指向的位置就是sentry值应该放置的位置。
arr[parent] = arr[child]; //孩子值上移。
parent = child; //更新parent使之指向孩子。
child = 2 * parent +1; //更新child使之指向新的parent的孩子。
}
arr[parent] = sentry;
}
//将[low..high]构建成最大堆
void make_heap(int arr[], int low, int high)
{
//找到最后一个非叶子(终端)结点。
int pos = (high-1) / 2;
//由最后一个非叶子结点开始,从下往上从右往左进行调整。
while(pos >= low)
{
heap_adjust(arr, pos, high);
--pos;
}
}
//结合构建最大堆函数和堆调整函数进行堆排序
void heap_sort(int arr[], int low, int high)
{
//构建成最大堆。
make_heap(arr, low, high);
//每趟将堆顶元素交换到尾部并分离,对剩下的元素周而复始进行相同的操作。遍量i的含义是大根堆的结束位置。
for(int i = high; i > low; --i)
{
swap(arr[low], arr[i]);
heap_adjust(arr, low, i-1); //[low+1,i-1]是符合大根堆的,通过调整使[low..i-1]符合大根堆。
}
}
主要通过heap_adjust()函数来构建大根堆,在这个函数中,会把数组调整成每个父节点都大于它的孩子节点,这样调整完成后,就会有根节点是数组的最大值,通过这个来进行排序。
七、归并排序
算法:也是使用了分割的思想,以中间点为分割,前后两部分比较,按大小放入另一个辅助数组,递归排序
时间复杂度:平均:O(N*log2N) 最好:O(N*log2N) 最坏:O(N*log2N)
空间复杂度:O(n)
稳定性:稳定
void two_way_merge(int sr[], int tr[], int low, int mid, int high)
{
//下标i,j,k分别标记第一二归并段和结果集合。
int i = low, j = mid+1, k = low;
while(i <= mid && j <= high) //循环继续的条件是i和j都未达到相应归并段的末尾。
{
if(sr[i] < sr[j])
{
tr[k++] = sr[i++];
}
else
{
tr[k++] = sr[j++];
}
}
//如果sr[low..mid]有剩余元素,进行处理。
while(i <= mid)
{
tr[k++] = sr[i++];
}
//如果sr[mid+1..high]有剩余元素,进行处理。
while(j <= high)
{
tr[k++] = sr[j++];
}
}
//将第一个参数所代表的元素就地排序,tmp[low..high]是临时空间。
//要求sr[low..high]与tmp[low..high]元素完全一样。
void rmerge(int sr[], int tmp[], int low, int high)
{
if(low == high) //只有1个元素默认已经有序不做任何处理。
{
return ;
}
else
{
int mid = low + ((high-low)>>1); //将[low..high]剖成两半
//先使tmp[low..mid]有序。
rmerge(tmp, sr, low, mid);
//再使tmp[mid+1..high]有序。
rmerge(tmp, sr, mid+1, high);
//最后将有序tmp[low..mid]和tmp[mid+1..high]归并到sr[low..high]。
two_way_merge(tmp, sr, low, mid, high);
}
}
//归并排序
void merge_sort(int arr[], int n)
{
if(NULL == arr || n <= 1)
return ;
int *tmp = new int[n];
assert(NULL != tmp);
for(int i = 0; i < n; ++i)
{
tmp[i] = arr[i];
}
rmerge(arr, tmp, 0, n-1);
delete[] tmp; //释放堆区内存。
}