概述
排序有内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存。
我们这里讲的是内部排序。
1、直接插入排序
将一个记录插入到已排序好的有序表中,从而得到一个新,记录数增1的有序表。即:先将序列的第1个记录看成是一个有序的子序列,然后从第2个记录逐个进行插入,直至整个序列有序为止。
算法实现:
//第一种排序算法:直接插入排序:如果数组基本有序,我们就用直接插入排序,越有序,时间复杂度越小,极端情况下为O(n)
void InsertSort(int arr[], int len)
{
//用临时量tmp保存关键值,从后向前找,比它小的或者走到了头,就将关键值放到下一个位置上
assert(arr != NULL);
if(NULL == arr)
return;
int tmp = 0;
int j = 0;
for(int i=1; i<len; i++)//需要排序的次数
{
tmp = arr[i];
for(j=i-1; j>=0; j--)//从后向前找
{
if(arr[j] > tmp)//比关键值大,则向后移动
{
arr[j+1] = arr[j];
}
else
{
break;//找到了比它小的值 退出
}
}
arr[j+1] = tmp;
}
}
算法效率分析:
1、时间复杂度O(n^2)
2、空间复杂度O(1)
3、算法稳定性:稳定的
2、希尔排序
希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。
算法实现:
static void Shell(int arr[], int n, int dk)//dk 分成多少间隔
{
int tmp = 0;
int j=0;
for(int i=dk; i<n; i++)//i开始的位置
{
tmp = arr[i]; //记录为哨兵
for(j=i-dk; j>=0; j=j-dk)
{
if(arr[j] > tmp)
{
arr[j+dk] = arr[j]; //元素后移
}
else
{
break;
}
}
arr[j+dk] = tmp; //插入到正确的位置
}
}
//按增量dk(n/2),n为要排序数的个数进行希尔排序
void ShellSort(int arr[], int n)
{
int dk = n / 2;
while(dk>=1)
{
Shell(arr, n, dk[i]);
dk /= 2;
}
}
算法效率分析:
1.时间复杂度O(n^1.3~1.5)
2.空间复杂度O(1)
3.稳定性:发生了跳跃交换,所以不稳定
3.冒泡排序
在要排序的一组数中,对当前还未排好序的范围内的全部数,自上而下对相邻的两个数依次进行比较和调整,让较大的数往后移动,较小的往前移动。即:每当两相邻的数比较后发现它们的排序与排序要求相反时,就将它们互换。
算法实现:
//冒泡排序:两两比较, 大的向后挪
void BubbleSort(int arr[], int len)
{
int tmp = 0;
for(int i=0; i<len-1; i++)//次数
{
for(int j=0;j+1<len-i; j++)//两两比较,大的向后挪
{
if(arr[j] > arr[j+1])
{
tmp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = tmp;
}
}
}
}
算法效率分析:
1、时间复杂度O(n^2)
2、空间复杂度O(1)
3、算法稳定性:稳定的
4、归并排序
归并排序(Merge)是将两个(或两个以上)有序表合并成一个新的有序表,即把待排序序列分为若干个子序列,每个子序列是有序的。然后再把有序子序列合并为整体有序序列。
归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。 将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。
算法实现:
//一次归并 以gap合并
static void Merge(int arr[], int len, int gap)//gap 标志几几合并
{
int *brr = (int*)malloc(sizeof(int) * len);
assert(brr != NULL);
int low1 = 0;
int high1 = low1 + gap -1;
int low2 = high1 + 1;
int high2 = low2+gap-1<len ? low2+gap-1 : len-1;//H2 有可能越界
int i = 0;
while(low2 < len)//有两个有序段
{
while(low1 <= high1 && low2 <= high2)//两个段内头指针都没走到尾巴
{
if(arr[low1] <= arr[low2])
{
brr[i++] = arr[low1++];
}
else
{
brr[i++] = arr[low2++];
}
}
//左边的段走到尾,那直接将右边的段内数据向下拷贝到brr内即可
while(low2 <= high2)
{
brr[i++] = arr[low2++];
}
//右边的段走到尾,那直接将左边的段内数据向下拷贝到brr内即可
while(low1 <= high1)
{
brr[i++] = arr[low1++];
}
//更改L1L2 H1H1,让指向接下来的两个有序段即可
low1 = high2 + 1;
high1 = low1+gap-1;
low2 = high1 + 1;
high2 = low2+gap-1<len ? low2+gap-1 : len-1;
}
//只有一个有序段
while(low1 < len)
{
brr[i++] = arr[low1++];
}
//将brr里的全部值拷贝到arr里面,然后将brr释放
for(int j=0; j<len; j++)
{
arr[j] = brr[j];
}
free(brr);
brr = NULL;
}
void MergeSort(int arr[], int len)
{
for(int i=1; i<len; i*=2)
{
Merge(arr, len, i);
}
}
算法分析:
1、时间复杂度:平均O(n log2 n)
2、空间复杂度:O(n)
3、算法稳定性:稳定
5、简单选择排序
在要排序的一组数中,选出最小(或者最大)的一个数与第1个位置的数交换;然后在剩下的数当中再找最小(或者最大)的与第2个位置的数交换,依次类推,直到第n-1个元素(倒数第二个数)和第n个元素(最后一个数)比较为止。
算法实现:
void SelectSort(int arr[], int len)
{
int minindex;//存放最小值的下标
for(int i=0; i<len-1; i++)
{
minindex = i;//循环开始前先认为待排序队列的第一个值是最小值
for(int j = i+1; j<len; j++)
{
if(arr[minindex] > arr[j])
{
minindex = j;
}
}
if(minindex != i)//这个if判断可以省略,不影响
{
int tmp = arr[minindex];
arr[minindex] = arr[i];
arr[i] = tmp;
}
}
}
算法分析:
1、时间复杂度O(n^2)
2、空间复杂度O(1)
3、算法稳定性:不稳定的
6、堆排序
初始时把要排序的n个数的序列看作是一棵顺序存储的二叉树(一维数组存储二叉树),调整它们的存储序,使之成为一个堆,将堆顶元素输出,得到n 个元素中最小(或最大)的元素,这时堆的根节点的数最小(或者最大)。然后对前面(n-1)个元素重新调整使之成为堆,输出堆顶元素,得到n 个元素中次小(或次大)的元素。依此类推,直到只有两个节点的堆,并对它们作交换,最后得到有n个节点的有序序列。称这个过程为堆排序。
调整要点:
1.从最后一个非叶子节点子树开始从后向前调整
2.调整的时候顺序是从上向下
3.升序(大顶堆),降序(小顶堆)
算法实现:
// O(logn)
void OneAdjust(int *arr, int len, int root)
{
int i = root;
int j = 2 * i + 1; // j是root的左孩子下标
int tmp = arr[i];
while (j < len)
{
// 找到左右孩子中较大的一个孩子
if (j + 1 < len && arr[j] < arr[j + 1]) // 判断有没有右孩子,如果右,判断右孩子是否大于左孩子
{
j = j + 1;
} // if判断执行完成之后,可以知道j是较大的孩子的下标
if (tmp > arr[j])
{
break;
}
arr[i] = arr[j];
i = j;
j = 2 * i + 1;
}
arr[i] = tmp;
}
void CreateHeap(int *arr, int len)
{
// root为最后一颗子树的根节点
int root = (len - 2) / 2;
for (; root >= 0; root--)
{
OneAdjust(arr, len, root);
}
}
void HeapSort(int *arr, int len)
{
CreateHeap(arr, len);
for (int i = 0; i < len - 1; ++i)
{
Swap(&arr[0], &arr[len - 1 - i]);
OneAdjust(arr, len-i-1 , 0);
}
}
算法分析:
1、时间复杂度:时间复杂度O(logn)
2、空间复杂度:O(1)
3、算法稳定性:不稳定
7、基数(桶)排序
基数(桶)排序, 低位优先,所有数据从低(个)位开始,依次放入到对应的10个桶内(入队),再依次从桶中将数据依次取出(出队),直到所有数据全部有序。 循环的次数和最大值的位数有关。
算法实现:
int GetMaxDigits(int *arr, int len)
{
int maxValue = arr[0];
for (int i = 0; i < len; ++i)
{
if (arr[i] > maxValue)
{
maxValue = arr[i];
}
}
// 计算最大的位数 maxValue = 1234
int digits = 0;
while (maxValue)
{
digits++;
maxValue /= 10;
}
return digits;
}
int GetDigitsValue(int value, int digits) // value = 1234 digits = 0 返回值为4
{
while (digits)
{
value /= 10;
digits--;
}
return value % 10;
}
// 时间复杂度: O(d*n) 空间复杂度: O(n) 稳定的
void RadixSort(int *arr, int len)
{
//1、获取所有数据中最大的关键字的个数
int maxDigits = GetMaxDigits(arr, len);
//2、申请关键字范围个数的队列 0 -- 9
SqQueue que[10];
for (int i = 0; i < 10; ++i)
{
InitQueue(&que[i], len + 1);
}
//3、循环根据各关键字将数据依次存储到对应的队列中,最后在将队列依次输出
for (int i = 0; i < maxDigits; ++i)
{
//3.1 将所有的数据依次存储在对应队列中
for (int j = 0; j < len; ++j)
{
int digValue = GetDigitsValue(arr[j], i);
PushQueue(&que[digValue], arr[j]);
}
//3.2 将队列中的所有数据依次输出到arr中
int index = 0;
for (int k = 0; k < 10; ++k)
{
while (!EmptyQueue(&que[k]))
{
PopQueue(&que[k], &arr[index]);
index++;
}
}
}
// 释放所有的队列
for (int i = 0; i < 10; ++i)
{
DestroyQueue(&que[i]);
}
}
算法分析:
1、时间复杂度:O(d*n)
2、空间复杂度:O(n)
3、算法稳定性:稳定
8、快速排序
1)选择一个基准元素,通常选择第一个元素或者最后一个元素,
2)通过一趟排序讲待排序的记录分割成独立的两部分,其中一部分记录的元素值均比基准元素值小。另一部分记录的 元素值比基准值大。
3)此时基准元素在其排好序后的正确位置
4)然后分别对这两部分记录用同样的方法继续进行排序,直到整个序列有序。
递归算法实现:
static int Partition(int arr[],int left, int right)
{
int tmp = arr[left];
while(left < right)//进来保证有两个值
{
while(left < right && arr[right] > tmp)
right--;
if(left == right)
{
break;
}
arr[left] = arr[right];
while(left < right && arr[left] <= tmp)
left++;
if(left == right)
{
break;
}
arr[right] = arr[left];
}
arr[left] = tmp;//arr[right] = tmp;
return left;//return right; 因为此时left == right
}
static void Quick(int arr[], int left, int right)
{
if(left < right)
{
int midindex = Partition(arr, left, right);
Quick(arr, left, midindex-1);
Quick(arr, midindex+1, right);
}
}
非递归实现:
void QuickNoR(int *arr, int start, int end)
{
SqStack st;
InitStack(&st);
PushStack(&st, start);
PushStack(&st, end);
while (!EmptyStack(&st))
{
int right = 0;
PopStack(&st, &right);
int left = 0;
PopStack(&st, &left);
int mod = OneQuick(arr, left, right);
if (mod - left > 1)
{
PushStack(&st, left);
PushStack(&st, mod - 1);
}
if (right - mod > 1)
{
PushStack(&st, mod + 1);
PushStack(&st, right);
}
}
}
void QuickSort(int *arr, int len)
{
QuickNoR(arr, 0, len - 1);
}
快速排序作为综合效率最好的内部排序算法,有三种优化方法:
第一种优化:如果有效个数特别少,直接调用直接插入排序
第二种优化:三数取中
第三种优化:防止完全有序,自己打乱一下
算法分析:
1、时间复杂度O(nlogn)
2、空间复杂度O(1)
3、算法稳定性:不稳定
排序算法对比: