手把手教你玩转排序算法
最近学习了各种排序算法,闲来无事总结一番,常用的排序算法分为以下几种:插入类排序,交换类排序,选择类排序,归并排序。其实还有基数排序,外部排序等,在本文只介绍插入类排序,交换类排序,选择类排序,归并排序。基数排序和外部排序较为复杂,不是简短篇幅可以介绍清楚。
插入类排序又分为:
- 直接插入排序
- 折半插入排序
- 希尔排序
直接插入排序:
首先以一个元素为有序的序列,然后将后面的元素一次插入到有序序列中合适的位置直到所有元素都插入有序序列。
废话不多说,直接上图理解
代码如下:
//直接插入排序
void InsertSort(int A[], int n) //n为待排序元素个数
{
int i, j;
for(i = 2; i <= n; i++) //默认第一个元素有序
{
if(A[i] < A[i - 1])
{
A[0] = A[i]; //数组第一个元素作为哨兵
for(j = i - 1; A[0] < A[j]; --j)
A[j + 1] = A[j];
A[j + 1] = A[0];
}
}
}
算法时间复杂度:O(n^2);
算法时间复杂度:O(1);
折半插入排序:
折半插入排序原理类似直接插入排序,唯一不同的是折半插入排序是先利用折半查找法找到待插入位置,然后再腾出位置将其插入
代码如下:
void HalfInsertSort(int A[], int n)
{
int i, j, low, high, mid;
for(i = 2; i <= n; i++)
{
A[0] = A[i];
low = 1;
high = i - 1;
while(low <= high) //折半查找待插入位置
{
mid = (low + high) / 2;
if(A[mid] > A[0])
high = mid - 1;
else
low = mid + 1;
}
//找到了待插入位置,接下来从后往前依次腾出位置
for(j = i - 1; j >= high + 1; --j)
A[j + 1] = A[j];
A[high + 1] = A[0]; //最终插入的位置
}
}
时间复杂度:O(n^2)
空间复杂度:O(1)
希尔排序:
希尔排序又称缩小增量排序,希尔排序的本质还是插入排序,只不过是把待排序序列分成几个子序列,再对这几个子序列进行直接插入排序。
其中缩小增量的增量一般是以序列长度n/2为开始,依次以n/2为增量,直到增量为1为止。例如10个数据序列,第一次增量为d1 = 10 / 2 = 5,第二次增量d2 = 5 / 2 = 2, 最后一次增量为1。讲解总是抽象的,看图说话:
初始时以5为增量将序列分成颜色不同的5组,在每组之间进行直接插入排序。
第二轮以2为增量将序列划分为颜色不同的2组,组间进行直接插入排序
最后一轮一1为增量,进行直接插入排序
经过直接插入排序可得最终结果
代码如下:
void ShellSort(int A[], int n)
{
int i, j, dk;
for(dk = n / 2; dk >= 1; dk = dk / 2) //初始增量为总长度一半,最后一次增量一定需要是1
{
for(i = dk + 1; i < n; ++i)
{
if(A[i] < A[i - dk])
{
A[0] = A[i];
for(j = i - dk; j > 0 && A[0] < A[j]; j -= dk) //每一组内部进行直接插入排序
A[j + dk] = A[j];
A[j + dk] = A[0];
}
}
}
}
希尔排序的优势是每一轮都会使整个序列变得越来越有序,最后一轮当增量为1的时候,整个序列几乎都是有序的,所有进行直接插入排序会提高排序的效率。
时间复杂度:约为O(n^1.3), 最坏情况为O(n^2).
空间复杂度:O(1)
交换类排序又分为:
- 冒泡排序
- 快速排序
冒泡排序:
冒泡排序是我们比较熟悉的一种算法,甚至在我们学习C语言的时候都已经学习过了,也是一个比较简单的排序算法,这里就不再重复说明原理,直接上代码。
void BubbleSort(int A[], int n)
{
for(int i = 0; i < n - 1; i++) //n - 1趟排序
{
bool flag = false;
for(int j = n - 1; j > i; j--)
{
if(A[j - 1] > A[j])
{
int temp = A[j - 1];
A[j - 1] = A[j];
A[j] = temp;
flag = true; //表明有数据交换
}
}
if(flag == false) //没有数据交换,表明已经有序
break;
}
}
时间复杂度:O(n^2)
空间复杂度:O(1)
快速排序:
快速排序是一种基于分治法的排序方法,每一趟快速排序选择序列中任一个元素作为枢轴,通常选第一个元素,将序列中比枢轴小的元素都移到枢轴前边,比枢轴大的元素都移到枢轴后边。
例如:
经过以上一趟快速排序将一个大问题划分为两个小问题(比46大的部分和比46小的部分),接下来对这两个部分分别进行快速排序,以此类推,直到序列有序。
代码如下:
int Partition(int A[], int low, int high)
{
int pivot = A[low]; //第一个元素作为枢轴
while(low < high)
{
while(low < high && A[high] >= pivot) --high; //从右往左找到第一个比枢轴小的元素
A[low] = A[high];
while(low < high && A[low] <= pivot) ++low; //从左往右找到第一个比枢轴大的元素
A[hihg] = A[low];
}
A[low] = pivot; //枢轴到达最终位置
return low; //返回枢轴位置
}
void QuickSort(int A[], int low, int high)
{
if(low < high)
{
int pivotpos = Partition(A, low, high);
QuickSort(A, low, pivotpos - 1); //分别对两个子问题进行分治操作
QuickSort(A, pivotpos + 1, high);
}
}
对于快速排序,其有一个最大的特点就是待排序序列越无序,效率越高,待排序序列越有序,效率越低,时间越久,因此
时间复杂度:最好情况下时间复杂度为O(nlogn), 最坏情况下时间复杂度为O(n^2)
空间复杂度:最好情况下为递归深度O(logn), 最坏情况下进行n-1次递归调用,为O(n)
选择类排序又分为:
- 简单选择排序
- 堆排序
简单选择排序:
简单选择排序原理较为简单,即在待排序列中选择当前最小的元素作为第i个元素,也是一种比较简单的排序,不再画图举例子,直接上代码
void SelectSort(int A[], int n)
{
int min, temp;
for(int i = 0; i < n - 1; i++) //依次从后面序列中选择当前最小的元素作为第一个元素,最后一个元素不需排序
{
min = i;
for (int j = i + 1; j < n; j++)
{
if (A[j] < A[min])
min = j;
}
if (min != i) //找到最小的,进行交换
{
temp = A[i];
A[i] = A[min];
A[min] = temp;
}
}
}
时间复杂度:O(n^2)
空间复杂度:O(1)
堆排序:
说到堆排序我们又不得不先了解堆
堆是一棵完全二叉树,而且满足任何一个非叶节点的值都不大于(或不小于)其左右孩子节点的值。
堆又分为大顶堆和小顶堆
大顶堆: 每个节点的值都不小于他的左右孩子结点的值
小顶堆: 每个节点的值都不大于他的左右孩子结点的值
有了以上堆的知识就可以更好的理解堆排序,堆排序是每次将无序序列调成一个堆,然后从堆中选择堆顶元素的值,这个值加入有序序列,无序序列减少一个,再反复调节无序序列,知道所有关键字都加入有序序列。如下例子:
- 建堆,对初始序列的完全二叉树调整成一个大顶堆(小顶堆),本例调整成大顶堆
2. 将堆顶结点和最后一个结点交换,完成了第一趟排序,输出最大值92
3. 重新建堆
4. 重复2,3步骤,知道将所有元素输出
代码如下:
void AdjustDown(int A[], int k, int len) //调堆
{
int i;
A[0] = A[k]; //A[0]为哨兵
for (i = 2 * k; i <= len; i *= 2)
{
if (i < len && A[i] < A[i + 1]) //如果有孩子比较大,考虑和右孩子交换
i++;
if (A[0] >= A[i]) //不小于孩子,不需要交换
break;
else
{
A[k] = A[i]; //与孩子结点交换
k = i; //继续向下检查,知道检查结束
}
}
A[k] = A[0];
}
void BuildMaxHeap(int A[], int len) //建立大顶堆
{
for (int i = len / 2; i > 0; i--)
AdjustDown(A, i, len);
}
void HeapSort(int A[], int len)
{
BuildMaxHeap(A, len);
for (int i = len; i > 1; i--)
{
int temp = A[i];
A[i] = A[1];
A[1] = temp;
cout << A[i] << " "; //由大到小输出
AdjustDown(A, 1, i - 1); //调堆
}
}
时间复杂度:建堆部分O(n),调堆部分O(nlogn),堆排序的时间复杂度为O(nlogn)
空间复杂度:O(1)
归并排序:(以二路归并为例)
若待排序表含有n个元素,则可以看成是n个有序的字表,每个字表长度为1,然后两两归并,得到n/2个长度为2或1的有序表;再两两归并,如此重复,直到合并成一个长度为n的有序表为止。
例如有原始序列如下:
- 首先将整个序列的每个元素看成是一个单独有序的子序列
- 两两归并,49和38归并成{38,49},65和97归并成{65,97},76和13归并成{13,76},27没有归并对象
- 两两归并,{38,49}和{65,97}归并成{38,49,65,97},{13,76}和27归并成{13,27,76}
- 两两归并,{38,49,65,97}和{13,27,76}归并成{13,27,38,49,65,76,97}
代码如下:
//归并排序
void Merge(int A[], int low, int mid, int high) //二路归并排序
{
int B[8]; //B数组大小和A数组大小一样,也可以动态开辟
int i, j, k;
for (int k = low; k <= high; k++)
B[k] = A[k]; //将A复制到B中
for ( i = low, j = mid + 1, k = i; i <= mid && j <= high; k++)
{
if (B[i] <= B[j])
A[k] = B[i++];
else
A[k] = B[j++];
}
while (i <= mid) A[k++] = B[i++]; //第一个表没有检测完
while (j <= high) A[k++] = B[j++]; //第二个表没有检测完
}
void MergeSort(int A[], int low, int high)
{
if (low < high)
{
int mid = (low + high) / 2; //从中间划分两个子序列
MergeSort(A, low, mid); //对左侧子序列进行递归排序
MergeSort(A, mid + 1, high); //对右侧子序列进行递归排序
Merge(A, low, mid, high); //归并
}
}
时间复杂度:O(n)
空间复杂度:O(n)