排序的算法有很多种,其关键在于根据待排序序列的特性选择合适的排序方式。下面将介绍不同的排序方式。
基本排序算法
基本排序算法主要包括插入排序,快速排序,冒泡排序等三种排序方式,下面将对这三种排序算法分别进行分析
插入排序
假定待排序数组序列为:data[5,2,3,8,1],将此数组从小到大排列
首先对数组的前面两个元素进行比较,若data[0]小于data[1]则不进行比较,若data[0]大于data[1],则将data[0]与data[1]互换,即将data[1]插入到合适的位置上去。接下来处理data[2],若data[2]同时小于data[0]与data[1],则将data[2]插入到数组头位置上去,那么data[0]与data[1]都需要向后移动一个位置。若data[2]介于两者之间,则将data[1]与data[2]交换位置。因此排序的过程就是依次处理数组元素data[i],并将其插入到合适的位置j上,使0<=j<=i.值得注意的是,在插入范围内,需要将大于data[i]的所有元素都要移动一个位置。对上述例子进行插入排序的步骤为:
[2,5,3,8,1]
[2,3,5,8,1]
[2,3,5,8,1]
[1,2,3,5,8]
C++实现
void insertionsort(vector<int> &data){
int n = data.size();
for(int i = 1; i < n; i++){
int temp = data[i];
int j = 0;
for(j = i; j>0 && temp<data[j-1]; j--){
data[j] = data[j-1];
}
data[j] = temp;
}
}
时间复杂度与空间复杂度的计算
首先考虑最好的情况,数组本来不需要排序,那么仅进行外层的for循环即可,此时的执行次数为n-1次,时间复杂度为O(n);
考虑最坏的情况,数组完全倒序,外层for循环依旧要进行n-1次,然而每个第i次外层for循环,内层for循环也要执行I次,故复杂度为:
void bubblesort(vector<int> &data){
int n = data.size();
bool again = true;
for(int i = 0; i < n-1 && again; i++){
for(int j = n-1, again = false; j > i; j--){
if(data[j] < data[j-1]){
swap(data[j], data[j-1]);
again = true;
}
}
}
}
1+2+3+...+n-1 = n(n-1)/2 = O(n^2);
另外,由于插入排序时申请的额外粗存空间与n无关,因此空间复杂度为O(1)。
因此,对于插入排序,时间复杂度为O(1),空间复杂度为O(n^2);
选择排序
选择排序的思路是先找到不合适的元素,再把其放在最终的合适位置上去。首先遍历数组找到数组中最小的元素,将此元素与data[0]进行交换,接下来寻找data[1]~data[n-1]中最小的元素,与data[1]进行交换,依次进行完成排序。
例:data[5,2,3,8,1]
data[1,2,3,8,5]
data[1,2,3,8,5]
data[1,2,3,5,8]
C++实现
void selectionsort(vector<int> &data){
int n = data.size();
for(int i = 0, j,min; i < n-1; i++){
for(j = i+1, min = i; j < n; j++)
if(data[j] < data[min]) min = j;
swap(data[i], data[min]);
}
}
时间复杂度与空间复杂度的计算
由于每次选择都要在data[i]~data[n-1]之间寻找最小的元素,因此无论何种情况,for循环的次数都是固定的,第一次进行外部for循环是,内部for循环执行n-1次,第二次外部for循环时内部for循环执行n-2次,因此总次数为:
(n-1)+(n-2)+...+2+1 = n(n-1)/2 = O(n^2);
由于也为申请过多的储存空间,因此空间复杂度为O(1);
因此,选择排序的时间复杂度为O(n^2),空间复杂度为O(1).
冒泡排序
冒泡排序与上述两种排序方式在处理方向上有所不同,冒泡排序采用了自底向上的排序方式,首先比较数据data[n-1]和数据data[n-2],若逆序则交换,接着比较data[n-2]与data[n-3],逆序则交换,一直比较到data[1]与data[0],经过这些操作,得以将最小的元素移动到数组的头位置。接下来继续比较,这次比较到data[1],将数组的次小元素移动到data[1]位置。
例:data[5,2,3,8,1]
data[5,2,3,1,8]
data[5,2,1,3,8]
data[5,1,2,3,8]
data[1,5,2,3,8]
data[1,2,5,3,8]
C++实现
void bubblesort(vector<int> &data){
int n = data.size();
bool again = true;
for(int i = 0; i < n-1 && again; i++){
for(int j = n-1, again = false; j > i; j--){
if(data[j] < data[j-1]){
swap(data[j], data[j-1]);
again = true;
}
}
}
}
时间复杂度与空间复杂度
根据上述代码可以看出,在最好情况下,数组已经为正序,则需要的比较次数为n-1,时间复杂度为O(n);
最坏情况下,数组为倒序,需要的比较次数为n-1+n-2+...+2+1 = n(n-1)/2 = O(n^2);
因此,冒泡排序的时间复杂度为O(n^2), 空间复杂度为(1);
高效排序算法
高效排序算法主要有希尔排序,堆排序,快速排序,归并排序,基数排序。
希尔排序
希尔排序算法的主要思想是将待排序数组拆分为几个数组分别进行排序,然后对这几个数组再进行排序,经验证使用这种办法的时间复杂度要小于O(n^2).首先每隔hi个数取一个元素,对这些元素进行插入排序,然后每隔h(i-1)个数取一个元素,依次排序直到h1 = 1;
通过例子解读希尔排序:data[10,8,6,20,4,3,22,1,0,15,16]
首先每个5个元素取一个元素,从第一个元素开始取,对元素[10,3,16]排序为[3,10,16], 原数组变为:
data[3,8,1,0,4,10,22,,6,20,15,16]
继续取5个元素,从第二个元素开始取,对元素[8,22]排序,原数组变为
data[3,8,1,0,4,10,22,,6,20,15,16],依次从第三个元素开始取,一直到如下情况:
data[3,8,1,0,4,10,22,,6,20,15,16],5-排序完成,h减小,开始进行3-排序。
3-排序与5-排序操作相同,一直进行到1-排序,希尔排序完成。
如何选定h的值呢,通常情况下我们有:
h(1) = 1 h(i+1)=3*h(i)+1;
直到h(i+2)>=n时停止得到h,然后依次上式递减h来进行希尔排序。
C++实现
void shellsort(vector<int> &data){
int n = data.size();
int h = 0, i = 0;
vector<int> increments;
for(h=1; h < n; ){
increments.push_back(h);
h = 3*h+1;
}
for(i = increments.size()-1; i >= 0; i--){
h = increments[i];
for(int hCnt = h; hCnt < 2*h; hCnt++){
//插入排序部分
for(int j = hCnt; j < n; j = j+h){
int temp = data[j];
int k = 0;
for(k = j; k-h>0 && temp < data[k-h]; k = k-h){
data[k] = data[k-h];
}
data[k] = temp;
}
}
}
}
注意上述代码插入部分与上面提到的插入排序几乎一致,区别仅在于递增的个数。其中,2*h代表了递增条件为h时,根据h取得的第二个元素的极限位置,即上述样例中蓝色部分。
时间复杂度与空间复杂度
希尔排序最优情况下时间复杂度为O(n^1.3),最坏情况下时间复杂度为O(n^2),空间复杂度为O(1).(不做证明)
堆排序
堆排序可以看作是一个选择排序的逆过程,依次找到数组中最大的元素放置于数组末端,区别在于寻找最大元素的方式,堆排序顾名思义,是将数组表示为堆的形式然后进行排序,那什么是堆呢?
堆
堆是一种特殊类型的二叉树,堆主要具有两个性质,一是每个节点的值大于等于每个子节点的值,二是该二叉树完全平衡,最后一层的叶子都位于最左侧的位置。上述表示的是最大堆,最小堆与上面的表示正好相反,最大堆的主要性质如下:
- 索引为i的左孩子的索引是(2*i+1)
- 索引为i的右孩子的索引是(2*i+2)
- 索引为i的父节点的索引为floor((i-1)/2)
使用堆排序的步骤如下:
- 将数组初始化为堆
- 交换a[0]与a[n-1]
- 挑战剩下的a[0]~a[n-2]为最大堆
- 重复2~3步
初始化堆
假设排序前数组为:data[20,30,90,40,70,110,60,10,100,50,80],n=11进行排序.
初始化i=(11-2)/2, i=4.data[20,30,90,40,70,110,60,10,100,50,80], 根据性质计算得到,data[4]的左孩子为data[9]=50, data[4]的右孩子为data[10]=80.故将data[10]与data[4]交换.由于2*10+1超出数组范围,故不再进行交换。
i=i-1=4-1=3. data[20,30,90,40,80,110,60,10,100,50,70], data[3]的左孩子为data[7]=10, data[3]的右孩子为data[8]=100,故将data[3]与data[8]交换,由于2*8+1超出数组范围,故不再进行交换。
i=i-1=3-1=2. data[20,30,90,100,80,110,60,10,40,50,70] , data[2]的左孩子为data[5]=110, data[2]的右孩子为data[6]=60,故将data[2]与data[5]进行交换,由于2*6+1超出数组范围,故不再进行交换。
i=i-1=1. data[20,30,110,100,80,90,60,10,40,50,70], data[1]的左孩子为data[3]=100,data[2]的右孩子为data[4]=80,故将data[1]与data[3]进行交换,data[20,100,110,30,80,90,60,10,40,50,70], 由于2*3+1=7未超出数组范围,故计算data[3]的左右孩子,data[3]的左孩子data[7]=10, data[3]的右孩子data[8]=40,故将data[3]与data[8]进行交换,得到data[20,100,110,40,80,90,60,10,30,50,70].由于8*3+1超出数组范围,故不再进行更新。
i=i-1=0. data[20,100,110,40,80,90,60,10,30,50,70], data[0]的左孩子为data[1]=100, data[0]的右孩子为data[2]=110, 故将data[0]与data[2]进行交换,得到data[110,100,20,40,80,90,60,10,30,50,70],由于2*2+1=5未超出数组范围,故计算data[2]的左右孩子,data[2]的左孩子data[5]=90, data[2]的右孩子data[6]=60,故将data[2]与data[5]进行交换,得到data[110,100,90,40,80,20,60,10,30,50,70].
排序,交换数据
首先将data[]中的第一个元素与最后一个元素交换,得到data[70,100,90,40,80,20,60,10,30,50,110], 此时最大的元素已经移动到数组末端。对剩下的数组部分初始化堆data[70,100,90,40,80,20,60,10,30,50],根据上式i=0的情况初始化堆。然后继续进行排序交换步骤,直到所有元素排序完成。
C++实现
void moveDown(vector<int> &a, int start, int end){
int c = start;
int l = start*2+1;
for(; l <= end; c=l,l=2*l+1){
if(l<end && a[l]<a[l+1])
l++;
if(a[c]>a[l])
break;
else{
swap(a[c],a[l]);
}
}
}
void heapsort(vector<int> &a, int n){
int i = (n-2)/2;
//数组初始化为堆
for(int j = i; j >= 0; j--){
moveDown(a,j,n-1);
}
for(int k = n-1; k>0; k--){
swap(a[0],a[k]);
moveDown(a, 0, k-1);
}
}
时间复杂度与空间复杂度的计算
堆排序的时间复杂度为O(nlogn), 空间复杂度为O(1),为不稳定排序。
快速排序
快速排序的核心思想是在数组中找到一个元素,根据这个元素对数组进行划分,继续在划分完成的数组内寻找元素,根据此元素将数组拆分,快速排序是一个递归的过程。划分一次的步骤如下:data[8,5,4,7,6,1,6,3,8,12,10]
1.选定基准,基准可以任意选取数组内元素内容,一般选取数组首个元素或者中间元素。
2.从左端开始遍历数组,一直遍历到数组元素小于基准时暂停遍历,开始从头开始遍历数组元素,一直到遍历到 大于基准时暂停遍历,此时交换左右两端遍历到的数组元素,持续上述过程直到i == j。交换i处与基准处的元素内容。随后在i处将数组拆分为两个。
3.在上述步骤拆分得到的两个数组内依次使用1,2步骤。
C++实现
void quiksort(vector<int> &a, int left, int right){
if(left > right)
return;
int i = left, j = right;
int mid = (left+right)/2;
int base = a[mid];
while(i != j){
while(a[j]>base && i < j)
j--;
while(a[i]<=base && i < j)
i++;
if(i<j)
swap(a[i],a[j]);
}
swap(a[mid],a[j]);
quiksort(a,left,i-1);
quiksort(a,i+1,right);
}
时间复杂度与空间复杂度
快速排序的时间复杂度为O(n^2), 空间复杂度为O(nlogn);
归并排序
归并排序与快速排序类似,都是先保证子序列有序,然后对各子序列进行排序,归并排序的例子如下data[9,6,7,22,20,33,16,20].
1. 划分子区间,划分到子区间内元素小于两个即停止划分。
2. 归并过程类似于将两个有序数组合并为一个有序数组,具体见代码
C++实现
void merge(vector<int>& nums, int l1, int r1, int l2, int r2 ) {
int i = l1; //左半部分起始位置
int j = l2; //右半部分起始位置
int n = (r1 - l1 + 1) + (r2 - l2 + 1); //要合并的元素个数
vector<int> temp(n); //辅助数组
int k = 0; //辅助数组其起始位置
while (i <= r1&&j <= r2) { //挑选两部分中最小的元素放入辅助数组中
if (nums[i] < nums[j])
temp[k++] = nums[i++];
else
temp[k++] = nums[j++];
}
//如果还有剩余,直接放入到辅助数组中
while (i <= r1)
temp[k++] = nums[i++];
while (j <= r2)
temp[k++] = nums[j++];
//更新原始数组元素
for (int i = 0; i < n;i++)
{
nums[l1 + i] = temp[i];
}
}
/*二路归并排序(递归实现)*/
void MergeSort(vector<int>& nums,int start, int end) {
if (start < end) {
int mid = (start + end)/2;
MergeSort(nums, start, mid);
MergeSort(nums, mid + 1, end);
merge(nums, start, mid, mid + 1, end);
}
}
时间复杂度与空间复杂度
归并排序的时间复杂度为O(nlogn),空间复杂度为O(1).为稳定排序