排序算法保持数字/记录的相对顺序,则该排序算法被称为稳定 ,即,如果您需要排序1 1 2 3,那么如果您不更改前两个1排序的顺序,则算法是稳定的。
按键值对进行排序时,这种区别变得更加明显。
假设您需要按照键的升序对以下键值对进行排序:
INPUT: (4,5), (3,2), (4,3),(5,4) ,(6,4)
OUTPUT1:(3,2), (4,5), (4,3),(5,4), (6,4) 稳定
OUTPUT2:(3,2), (4,3), (4,5),(5,4), (6,4) 不稳定(改变了原来的相对顺序)
对排序算法的选取需要考虑的因素有以下四点:
1.待排序的记录数目n的大小;
2.记录本身数据量的大小,也就是记录中除关键字外的其他信息量的大小;
3.关键字的结构及其分布情况;
4.对排序稳定性的要求。
设待排序元素的个数为n:
1)当n较大,应采用时间复杂度为O(nlogn)的排序方法:快速排序、堆排序、归并排序
2)当n较大,内存空间允许,且要求稳定性 ——归并排序
3)当n较小,采用直接插入或直接选择排序。
5)一般不使用或不直接使用传统的冒泡排序。
[2 7 6 12 4 3 9]
一、两种插入排序:将元素插入到合理位置
1.插入排序(打扑克齐牌)
思想:将第一个元素看做有序序列,把剩下的第二个元素到最后一个元素依次扫描(N-1趟),将扫描到的每个元素插入有序序列的适当位置-----与前一个位置元素比较,如果小于前一个位置元素,则将前一个位置元素右移一位,再与当前位置的前一个元素比较…元素位置与下标顺序对应(如果待插入元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面)
void insertSort(int arr[],int len)//待排序列及其长度
{
for(int i = 1; i < len; i++) //共N-1趟,每一趟放置位置i上的元素到正确位置
{
int temp = arr[i]; //位置i上的元素
int j = i-1; //j初始为i的前一个位置i-1
while((j >= 0) && (temp < arr[j]))//位置i的元素比i-1位置的元素小
{
arr[j+1] = arr[j];//大的元素右移,temp悬空插到合适位置
j--; //可能接着比较i-2位置的元素......
}
arr[j+1] = temp;//把位置i上的元素放在正确位置,跳过while则此元素不变
}
}
测试用例:
#include "Sort.h"
void Print(int a[],int len)
{
for(int i = 0; i < len; ++i)
{
cout<<a[i]<<" ";
}
cout<<endl;
}
void test()
{ //升序排序
int a [] = {2,7,6,12,4,3,9};
int len = sizeof(a)/sizeof(a[0]);
cout<<"before sort :";
Print(a,len);
InsertSort(a,len);
cout<<"after sort :";
Print(a,len);
}
int main ()
{
test();
return 0;
}
2.希尔排序(缩小增量排序)
思想:把序列按下标的一定增量分成若干组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个序列恰被分成一组,进行最后一次整体插入排序,算法便终止。我们选择增量gap=length/2,缩小增量继续以gap = gap/2的方式。
void ShellSort(int arr[], int len)
{
int gap, i, j;
int temp;
for (gap = len >> 1; gap > 0; gap >>= 1)//划分若干组
{
for (i = gap; i < len; i++)//每组内部是插入排序
{
temp = arr[i];
j = i - gap;
while( j >= 0 && temp < arr[j])
{
arr[j + gap] = arr[j];
j -= gap;
}
arr[j + gap] = temp;
}
}
}
二、两种选择排序:将最小/最大元素交换放到末尾
3.选择排序
思想:首先在序列中找到最小元素,存放到排序序列的起始位置(第一个位置)-交换。
再从剩余未排序元素中继续寻找最小元素,然后放到已排序序列的末尾(第二个位置)。
重复第二步,直到找出所有元素放到排序排列中。
void swap(int *a,int *b) //交换两个数
{
int temp = *a;
*a = *b;
*b = temp;
}
void SelectionSort(int arr[], int len)
{
int i,j;
for (i = 0 ; i < len - 1 ; i++)
{
int min = i;//将第一个元素初始化为最小元素,每一轮min初试值向右移动1个位置
for (j = i + 1; j < len; j++) //从第二个元素开始遍历未排序的元素
{
if (arr[j] < arr[min]) //找到剩余未排序序列中的最小值
min = j; //记录当前最小值元素下标
}
swap(&arr[min], &arr[i]); //将最小元素与第二个元素做交换
//swap(arr[min], arr[i]); //也可以直接使用C++库函数
}
}
4.堆排序
思想:堆排序是一种利用堆这种数据结构的概念来排序的选择排序。
堆是一种完全二叉树,分为两种方法:
大顶堆:每个节点的值都大于或等于其子节点的值,在堆排序算法中用于升序排列;
小顶堆:每个节点的值都小于或等于其子节点的值,在堆排序算法中用于降序排列;
a.将无需序列(数组)构建成一个堆,根据升序降序需求选择大顶堆或小顶堆;
b.将堆顶元素与末尾元素交换,将最大元素"沉"到数组末端;
c.重新调整结构(构建堆),使其满足堆定义,然后继续交换堆顶元素与当前未排序末尾元素,反复执行调整+交换步骤,直到整个数组序列有序。//构建就是调整
void HeapSort(int arr[], int len)
{
//初始化,i从最后一个父节点(即最后一个非叶节点)开始调整-即构建堆
for (int i = len / 2 - 1; i >= 0; i--)
max_heapify(arr, i, len - 1);
//将堆顶元素和未排序末尾元素(已经排好的元素前一位)交换,将最大值放到末尾
//再重新调整使得满足堆定义,继续将堆顶元素与未排序末尾元素交换,直到排序完毕
for (int i = len - 1; i > 0; i--)
{
swap(arr[0], arr[i]);
max_heapify(arr, 0, i - 1);
}
}
//构建/调整为堆
void max_heapify(int arr[], int start, int end)//调整的开始节点下标和结束节点下标
{
// 建立父节点指针和子节点指针
int dad = start;
int son = dad * 2 + 1;
while (son <= end)
{ // 若两子节点指针在范围内才做比较
if (son + 1 <= end && arr[son] < arr[son + 1])
son++; //先比较两个子节点大小,选择最大的保存
if (arr[dad] > arr[son])
return;//如果父节点大于较大子节点则代表调整完毕,直接跳出函数
else
{ //否则交换父子内容再继续子节点和孙节点比较
swap(arr[dad], arr[son]);//C++库函数
dad = son;
son = dad * 2 + 1;
}
}
}
三、两种交换排序
5.冒泡排序
思想:对相邻的元素进行两两比较,顺序相反(前大于后)则进行交换,这样,第一轮会将最大元素“浮”到数组右端,继续对剩余N-1个元素执行以上操作,每一轮获得一个最大元素,最终达到完全有序。
void BubbleSort(int arr[], int len)
{
int i, j, temp;
for (i = 0; i < len - 1; i++)//每一轮都将未排序数组的最大值交换到数组右边
{
for (j = 0; j < len - 1 - i; j++)//每一轮的未排序数组长度都在往前减小1
{
if (arr[j] > arr[j + 1])
{ //swap(arr[j], arr[j+1]);
temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
6.快速排序
思想:选择基准值将序列分成两部分,小于基准值的都放在左边,大于基准值的都放在右边。对于两部分,再分别递归的进行下去,直到所有序列排序。
void QuickSort(int arr[], int low, int high) //快排母函数
{
if (low < high)
{
int pivot = Paritition(arr, low, high);//获取基准值下标,同时分成两部分
QuickSort(arr, low, pivot - 1);
QuickSort(arr, pivot + 1, high);
}
}
Paritition(int arr[], int low, int high)
{
int pivot = arr[low];//取基准值为第一个元素(临时值)
while (low < high)
{ //当队尾元素大于等于基准值是,向前移动指针
while (low < high && arr[high] >= pivot)
--high;
arr[low] = arr[high];//找到小于基准值的元素了,赋给low元素
//当队首元素小于等于基准值,向后移动指针
while (low < high && arr[low] <= pivot)
++low;
arr[high] = arr[low];//找到大于基准值的元素了,赋给high
}
//跳出循环时low=high,此时的low或high就是基准值的正确索引位置
arr[low] = pivot;
return low;
}
7.归并排序
思想:分治策略,先分开后合并
1)申请临时空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列;
(lc88.合并两个有序数组-双指针)
2)设定两个指针,最初位置分别为两个已经排序序列的起始位置,再设定一个指针指向合并序列;
3)比较两个指针所指向的元素,选择较小的元素放入到合并序列,并移动指针到下一位置;
4)重复步骤 3 直到某一排序序列的指针达到其序列尾;
5)将另一序列剩下的所有元素直接复制到合并序列尾。
void MergeSort(int arr[], const int len)
{
int temp[len];//临时合并序列
merge_sort_recursive(arr, temp, 0, len - 1);//无序序列、合并序列、首尾下标
}
void merge_sort_recursive(int arr[], int temp[], int start, int end)
{
if (start >= end)
return;//递归结束,拆分完成,开始合并两部分的已排序序列
int mid = ((end - start) >> 1) + start;
int start1 = start, end1 = mid; //序列1的起点、终点指针
int start2 = mid + 1, end2 = end;//序列2的起点、终点指针
merge_sort_recursive(arr, temp, start1, end1);//不断拆分,直到两部分只包含1个元素
merge_sort_recursive(arr, temp, start2, end2);//下一行开始合并,两部分越来越长
int i = start;
while (start1 <= end1 && start2 <= end2)//较小元素放入合并序列,直到某一序列末尾
temp[i++] = arr[start1] < arr[start2] ? arr[start1++] : arr[start2++];
//将另一序列中剩下的元素复制到合并序列
while (start1 <= end1)
temp[i++] = arr[start1++];
while (start2 <= end2)
temp[i++] = arr[start2++];
//将临时合并序列的元素拷贝回原数组
for (i = start; i <= end; i++)
arr[i] = temp[i];
}
8.基数排序
思想:适合于有不同位数的大小数字!
先找十个桶:0~9
第一轮按照元素的个位数排序,依次放于桶中,之后,从所有桶中按照从左向右,从上到下的顺序依次取出元素,组成新的数组。
在新的数组中,进行第二轮,按照十位数排序,依次存放于桶中,再组成新数组。
在新的数组中,进行第三轮,按照百位数排序,依次存放于桶中,再组成新数组。
。。。。。。
int maxbit(int data[], int n) //辅助函数,求数据的最大位数,也是排序轮数
{
int maxData = data[0]; //初试最大数为第一个数
//先求出最大数,再求其位数,这样与原先依次对每个数判断其位数相比,稍微优化点
for (int i = 1; i < n; ++i)//遍历数组,保留找到最大数
{
if (data[i] > maxData)
maxData = data[i];
}
int d = 1; //初始化最大数只有1位
int p = 10;
while (maxData >= p)//最大数字比10大,至少有2位(个位 + 十位)
{
maxData /= 10;
++d;
}
return d;//返回最大数字的位数
}
void RadixSort(int data[], int n) //基数排序
{
int d = maxbit(data, n);//求出数组中最大数字的位数,及后续的排序轮数-每一位排一趟
int *tmp = new int[n]; //临时数组
int *count = new int[10]; //计数器,即用于存储的10个桶,代表0~9
int i, j, k;
int radix = 1;
for(i = 1; i <= d; i++) //进行d次排序(最大数字有d位数)
{
for(j = 0; j < 10; j++)
count[j] = 0; //每次分配前清空计数器
for(j = 0; j < n; j++)//遍历所有数字
{
k = (data[j] / radix) % 10; //每个数字的个位,下一轮则是十位,再下一轮百位
count[k]++;//统计每个桶中装了多少数字
}
for(j = 1; j < 10; j++)
count[j] = count[j - 1] + count[j]; //将tmp数组中的位置依次分配给每个桶
for(j = n - 1; j >= 0; j--) //将所有桶中的数字依次收集到tmp数组中
{
k = (data[j] / radix) % 10;
tmp[count[k] - 1] = data[j];
count[k]--;
}
for(j = 0; j < n; j++) //将临时数组的内容复制到data中
data[j] = tmp[j];
radix = radix * 10;
}
delete []tmp;
delete []count;
}