前言
排序是将一组元素,重新排列成按关键字有序的序列。
- 排序的稳定性:如果序列中有两个相同的关键字,排序之后这两个关键字的顺序没有发生变化,说明该排序是稳定的,否则排序是不稳定的。
- 内部排序和外部排序:待排序列存放在计算机随机存储器中进行排序的过程(也就是内存中进行的排序)叫内部排序,如果待排的序列非常大,某一时刻只允许序列中的部分在内存中,需要借助外存来排序,叫外部排序。
下面记录各种排序的实现,方便日后查阅。
1.插入排序
插入排序就是将一个元素插入到有序序列中,从而得到一个新的有序序列,适用于少量数据排序,时间复杂度为O(n^2),是稳定的排序方法。
1.1 直接插入排序
void insertSort(ElemType* arr, int len)
{
int temp,j,i;
for(i=1;i<len;i++)
{
temp = arr[i];
j = i-1;
while(j>=0 && temp<arr[j])
{
//只要没人temp大,向后腾位置
arr[j+1] = arr[j];
j--;
}
//多减了一次导致的while退出
arr[j+1] = temp;
}
}
1.2 折半插入排序
虽然也是插入排序,但是在找插入位置的时候使用折半查找。
void BinarySort(ElemType* array, int n)
{
for(int i=1; i<n; i++)
{
int left=0;
int right=i-1;
int temp = array[i]
while(left<=right)
{
int mid = (left+right)/2;
if(temp>array[mid])
left = mid+1;
else
right = mid-1;
}
for(int j=i;j>=left;j--)
array[j] = array[j-1];
array[left] = temp;
}
}
1.3 希尔排序
希尔排序也是插入排序的一种,也称为缩小增量排序,是直接插入排序的一种更高效改进版本。希尔排序是一种非稳定排序算法,希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来愈多,当增量减少至1时,所有序列都被分为一组,排序结束。
void shellsort(ElemType*array, int len)
{
int d = len / 2; //分组(增量初始值)
int i, j;
int temp;
while(d>0) //取决于希尔排序要进行的次数(每一次都有相应的分组情况,即有不同的增值)
{
for(i = d; i<len; i++) //对该次希尔排序进行插入排序
{
temp = arr[i];
for(j = i-d; j>=0 && temp<arr[j]; j = j-d) //将每次的元素插入(由有序元素的个数来决定)
arr[j+d] = arr[j];
}
d = d/2; //重新分组
}
}
3.冒泡排序
冒泡排序算是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就将他们交换过来。每一次都会找一个最大的,放到末尾。
冒泡排序改进:如果在某次的遍历中没有数据交换,说明整个数组已经有序。
void bulletSort(ElemType* arr, int len)
{
int temp;
for(int i=0; i<len-1; i++)
{
for(int j=0; j<len-1; j++)
{
if(arr[j]>arr[j+1])
{
temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
4. 快速排序
快速排序(QuickSort)是对冒泡排序的一种改进。它的基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序的过程可以 递归进行,以此达到整个数据变成有序序列。
#include <stdio.h>
typedef int ElemType;
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); //递归操作右边元素
}
int main()
{
int arr[10] = {3,4,5,2,5,2,5,10,7,9};
quickSort(arr, 0, 9);
for(int i=0; i<10; i++)
printf("%3d", arr[i]);
printf("\n");
return 0;
}
5.简单选择排序
思想:每一趟从n-i+1(i=1,2,…n-1)个记录中选择最小关键字作为有序序列中的第i个记录。
void selctSort(int *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[j];
arr[i] = arr[j];
arr[j] = temp;
}
}
}
}
6.堆排序
堆的插入——每一次插入都是将新数据放在数组最后,而这个新数据的父节点到根节点必定是一个有序的数列,因此只要将这个新数据插入到这个有序数列中即可。
堆的删除——将最后一个数据的值赋给根节点,然后从根节点开始进行一次从上向下的调整。调整时先在左右儿子节点中找到最小的,如果父节点比这个最小的子节点还小说明不需要调整了,反之将父节点和它交换后再考虑后面的节点。相当于从根节点开始将一个数据在有序数列中进行“下沉”。
因此,堆的插入和排序非常类似直接插入排序,只不过实在二叉树上进行插入过程。
//堆排序
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;
}
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);//归并排序
}
总结
排序图表: