一、排序算法概览
可以在VisuAlgo或者站长辅助工具中查看这些排序算法的动态演示过程
二、算法实现
1、选择排序
选择排序(Selection sort)是一种简单直观的排序算法。它的工作原理是每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完。 选择排序是不稳定的排序方法。
void selectSort(ElemType arr[], int len) //选择排序
{
int temp;
for (int i = 0; i < len - 1; ++i)
{
for (int j = i + 1; j < len; ++j)
{
if (arr[i]>arr[j])
{
temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
}
}
2、冒泡排序
冒泡排序(Bubble Sort),是一种计算机科学领域的较简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越大的元素会经由交换慢慢“浮”到数列的顶端。
冒泡排序改进1:在某次遍历中如果没有数据交换,说明整个数组已经有序。因此通过设置标志位来记录此次遍历有无数据交换就可以判断是否要继续循环。
冒泡排序改进2:记录某次遍历时最后发生数据交换的位置,这个位置之后的数据显然已经有序了。因此通过记录最后发生数据交换的位置就可以确定下次循环的范围了。
void bulletSort(ElemType arr[], int len) //冒泡排序
{
int temp;
for (int i = 0; i < len - 1; ++i)
{
for (int j = 0; j < len - 1 - i; ++j)
{
if (arr[j]>arr[j + 1])
{
temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
3、插入排序
插入排序基本操作就是将一个数据插入到已经排好序的有序数据中,从而得到一个新的、个数加一的有序数据,算法适用于少量数据的排序,时间复杂度为O(n^2)。是稳定的排序方法。插入排序的基本思想是:每步将一个待排序的纪录,按其关键码值的大小插入前面已经排序的文件中适当位置上,直到全部插入完为止。
void insertSort(ElemType arr[], int len) //插入排序
{
int temp,j,i;
//形式1
for (i = 1; i < len; ++i)
{
temp = arr[i];
j = i - 1;
while (j >= 0 && temp < arr[j])//j指向的是有序序列
{
arr[j + 1] = arr[j];
--j;
}
arr[j + 1] = temp;
}
//形式2
//for (i = 1; i < len; ++i)
//{
// temp = arr[i];
// for (j = i - 1; j >= 0 && temp < arr[j]; --j)//j指向的是有序序列
// {
// arr[j + 1] = arr[j];
// }
// arr[j + 1] = temp;
//}
//形式3
/*for (i = 1; i < len; ++i)
{
temp = arr[i];
for (j = i - 1; j >= 0; --j)
{
if (temp < arr[j])
{
arr[j + 1] = arr[j];
}
else
break;
}
arr[j + 1] = temp;
}*/
//形式4
/*for (int i = 1; i < len; ++i)
{
for (int j = i - 1; j >= 0; --j)
{
if (arr[j + 1] < arr[j])
{
temp = arr[j + 1];
arr[j + 1] = arr[j];
arr[j] = temp;
}
else
break;
}
}*/
}
4、快速排序
快速排序(Quicksort)是对冒泡排序的一种改进。它的基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
void quickSort(ElemType arr[], int left, int right)//快速排序
{
int i = left, j = right;
int temp;
if (left >= right) return;
while (i <= j)
{
while (i <= j&&arr[left] >= arr[i]) ++i;//找出左边比arr[left]大的元素
while (i <= j&&arr[left] <= arr[j]) --j;//找出右边比arr[left]小的元素
if (i < j)//交换找到的元素
{
temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
i++;//交换完之后移向下一个位置
--j;
}
}
//经过循环后在j位置就是标杆的位置,这个位置左边都不大于该值,该位置右边都不小于该值
temp = arr[left];
arr[left] = arr[j];
arr[j] = temp; //交换
quickSort(arr, left, j - 1); //递归操作左边元素
quickSort(arr, j+1, right); //递归操作右边元素
}
5、希尔排序
希尔排序(Shell Sort)是插入排序的一种。也称缩小增量排序,是直接插入排序算法的一种更高效的改进版本。希尔排序是非稳定排序算法。希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。
void shellSort(ElemType arr[], int len) //希尔排序
{
int d = len / 2; //分组(增量置初值)
int i, j;
int temp;
while (d > 0) //由希尔排序要进行的次数来决定(每一次都有相应的分组情况,即有不同的增值)
{
for (i = d; i < len; ++i) // 对该次希尔排序(分组情况)进行插入排序(由要进行插入排序的元素个数来决定)
//本来一个插入排序从1开始,这里有d组插入排序,所以从d开始
//这个是d组一起进行插入排序,即等到每一组都插入第i个元素后,才会进行插入第i+1个元素
{
temp = arr[i];
for (j = i - d; j >= 0 && temp < arr[j]; j = j - d) //将每次的元素插入(由有序元素的个数来决定)
arr[j + d] = arr[j];
arr[j + d] = temp;
}
d = d / 2; //从新分组
}
}
6、桶排序(基数排序)
基数排序,第一步根据数字的个位分配到每个桶里,在桶内部排序,然后将数字再输出(串起来);然后根据十位分桶,继续排序,再串起来。直至所有位被比较完,所有数字已经有序。
void radixSort(ElemType arr[], int len)//桶排序(基数排序)
{
int temp[10][20];
int index;
for (int n = 1; n <= 100; n *= 10)//数据中最大数是几位,就要进行几次循环
{
for (int x = 0; x < 10; ++x) //初始化 为了方便查找到存放的数据
{
for (int y = 0; y < 20; ++y)
{
temp[x][y] = -1;
}
}
for (int i = 0; i < len; ++i) //存放数据
{
index = (arr[i] / n) % 10;
temp[index][i] = arr[i];
}
int count = 0;
for (int i = 0; i < 10; ++i) //将该次排序后的结果放回原数组
{
for (int j = 0; j < 20; ++j)
{
if (temp[i][j] != -1)
arr[count++] = temp[i][j];
}
}
}
}
7、归并排序
归并排序是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
void mergeInArr(ElemType arr[], int left, int mid, int right)
{
//注意此时左右空间都是各自有序
int length = right - left + 1; //辅助数组长度
int *p = new int[length]; //构建辅助数组
memset(p, 0, sizeof(int)*length); //给辅助数组赋值
int low = left; //记录左区间的起始位置
int hig = mid + 1; //记录右区间的起始位置
int index = 0; //辅助空间下标
while (low <= mid&&hig <= right) //左右区间都没有比较完,都有数据
{
while (low <= mid&&arr[low] <= arr[hig])//如果左边区间没有越界,并且左区间的数值<=右区间的数值
p[index++] = arr[low++]; //将小的数据存入辅助空间
while (hig <= right&&arr[low] > arr[hig]) //如果右边区间没有越界,并且左区间的数值>右区间的数值
p[index++] = arr[hig++]; //将小的数据存入辅助空间
}
//到这一步,证明起码有一个区间是合并进了辅助数组
if (hig <= right)//证明右区间并没有完成合并,左区间是完成合并,把右区间剩下的数据直接拷贝到辅助数组即可(此时右区间剩下的数据比辅助空间的数据大)
memcpy(&p[index], &arr[hig], sizeof(int)*(right - hig + 1));
if (low <= mid)
memcpy(&p[index], &arr[low], sizeof(int)*(mid - low + 1));
//将排完序的值传回给原数组
memcpy(&arr[left], p, sizeof(int)*length); //这里&arr[left]要特别注意,不能写成arr
delete[]p;
}
void merge(ElemType arr[], int left, int right)
{
int mid;
if (left >= right) return; //递归出口 这里就不需要递归了
mid = ((right - left) >> 1) + left; //查找到中间值 将数组分成两部分 左区间和右区间
//************ 归操作 **********//
merge(arr, left, mid); //左区间
merge(arr, mid + 1, right); //右区1间
//************** 并操作 **************//
mergeInArr(arr, left, mid, right); //将归操作后单个有序区间合并
}
void mergeSort(ElemType arr[], int len)
{
merge(arr, 0, len - 1);//归并排序
}
函数说明
(1)使用以下函数需要加头文件#include<memory.h>
(2)memset 函数:内存逐字节赋值,有三个参数,第一个参数是哪个内存,第二个参数赋什么值,第三个参数这个内存需要赋多大的内存。
(3)memcpy内存拷贝函数,有3个参数,表示把第二个参数的首地址里面的内容拷贝到第一个参数表示的首地址里面,拷贝大小为第三个参数表示的大小
8、堆排序
堆的插入就是——每次插入都是将新数据放在数组最后,而从这个新数据的父结点到根结点必定是一个有序的数列,因此只要将这个新数据插入到这个有序数列中即可。
堆的删除就是——堆的删除就是将最后一个数据的值赋给根结点,然后再从根结点开始进行一次从上向下的调整。调整时先在左右儿子结点中找最小的,如果父结点比这个最小的子结点还小说明不需要调整了,反之将父结点和它交换后再考虑后面的结点。相当于从根结点开始将一个数据在有序数列中进行“下沉”。
因此,堆的插入和删除非常类似直接插入排序,只不是在二叉树上进行插入过程。所以可以将堆排序形容为“树上插”
更加详细的过程可以查看博文:堆排序算法(图解详细流程)
//堆排序
public static void heapSort(int[] arr) {
//构造大根堆
heapInsert(arr);
int size = arr.length;
while (size > 1) {
//固定最大值
swap(arr, 0, size - 1);
size--;
//构造大根堆
heapify(arr, 0, size);
}
}
//构造大根堆(通过新插入的数上升)
public static void heapInsert(int[] arr) {
for (int i = 0; i < arr.length; i++) {
//当前插入的索引
int currentIndex = i;
//父结点索引
int fatherIndex = (currentIndex - 1) / 2;
//如果当前插入的值大于其父结点的值,则交换值,并且将索引指向父结点
//然后继续和上面的父结点值比较,直到不大于父结点,则退出循环
while (arr[currentIndex] > arr[fatherIndex]) {
//交换当前结点与父结点的值
swap(arr, currentIndex, fatherIndex);
//将当前索引指向父索引
currentIndex = fatherIndex;
//重新计算当前索引的父索引
fatherIndex = (currentIndex - 1) / 2;
}
}
}
//将剩余的数构造成大根堆(通过顶端的数下降)
public static void heapify(int[] arr, int index, int size) {
int left = 2 * index + 1;
int right = 2 * index + 2;
while (left < size) {
int largestIndex;
//判断孩子中较大的值的索引(要确保右孩子在size范围之内)
if (arr[left] < arr[right] && right < size) {
largestIndex = right;
} else {
largestIndex = left;
}
//比较父结点的值与孩子中较大的值,并确定最大值的索引
if (arr[index] > arr[largestIndex]) {
largestIndex = index;
}
//如果父结点索引是最大值的索引,那已经是大根堆了,则退出循环
if (index == largestIndex) {
break;
}
//父结点不是最大值,与孩子中较大的值交换
swap(arr, largestIndex, index);
//将索引指向孩子中较大的值的索引
index = largestIndex;
//重新计算交换之后的孩子的索引
left = 2 * index + 1;
right = 2 * index + 2;
}
}
//交换数组中两个元素的值
public static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
三、总结
此处使用More Windows图解来对上述的算法进行总结,如果对图解中算法的含义不是很理解可以参考博文:各个排序算法的时间复杂度和稳定性,快排的原理
排序图表
More Windows图解七种经典的排序算法