文章目录
冒泡排序(Bubble Sort)
算法思路:依次比较两个相邻的元素,大的往后,小的往前,每一趟比较都把最大的元素放到末尾,越小的元素会经由交换慢慢“浮”到数列的顶端,因而称之为冒泡排序
时间复杂度:O(n^2)
空间复杂度:O(1)
稳定性:冒泡排序的比较是相邻的两个元素比较,交换也发生在这两个元素之间。所以,如果两个元素相等,是不会再交换的;如果两个相等的元素没有相邻,那么即使通过前面的两两交换把两个相邻起来,这时候也不会交换,所以相同元素的前后顺序并没有改变,所以冒泡排序是一种稳定排序算法
冒泡排序示意图:
C代码:
void BubbleSort(int *arr, int len) //冒泡排序
{
//两两比较,小的往前,大的往后,每一趟把最大的放到最后
int tmp;
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;
}
}
}
}
选择排序(Selection Sort)
算法思路:第一次从待排序的数据元素中选出最小的一个元素,存放在序列的起始位置,然后再从剩余的未排序元素中寻找到最小元素,然后放到已排序的序列的末尾,即每次从待排序数据中选个"最小值"和"第一个交换"
时间复杂度:O(n^2)
空间复杂度:O(1)
稳定性:不稳定,有跳跃式地交换数据,例:序列5 8 5 2 9,第一遍选择第1个元素5会和2交换,那么原序列中两个5的相对前后顺序就被破坏了,所以选择排序是一个不稳定的排序算法
选择排序示意图:
C代码:
void SelectSort(int *arr, int len) //选择排序
{
//每一趟选出最小的放到最前面
int minindex; //保存最小值的下标
for (int i = 0; i < len; i++)
{
minindex = i;
for (int j = i +1; j < len; j++)
{
if (arr[j] < arr[minindex])
{
minindex = j;
}
}
//选出来的最小值与“第一个”进行交换
int tmp;
tmp = arr[i];
arr[i] = arr[minindex];
arr[minindex] = tmp;
}
}
(直接)插入排序(Insertion Sort)
算法思路:通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。即将待排序数据看成两个部分,左边部分为已经排序好的数据,右部分为待排序数据,从右边的数据中取一个数,插入到左边的合适位置使左边部分始终保持有序
时间复杂度:最坏O(n^2),最好O(n),平均O(n^2)
插入排序待排序数列越有序越快,完全有序时间复杂度为O(n)
空间复杂度:O(1)
稳定性:稳定
插入排序示意图:
C代码:
void InsertSort(int *arr, int len)
{
int i;
int j;
int tmp;
//每次从右部分取出一个插入到左边的合适位置
for (i = 1; i < len; i++)//遍历右部分
{
tmp = arr[i];
for (j = i - 1; j >= 0; j--)//j是左部分数列游标
{
if (tmp >= arr[j])//右边取出的大于左边的
{
break;
}
else//右边取出的小于左边的
{
arr[j + 1] = arr[j];//左部分数列后移
}
}
arr[j+1] = tmp;//把右边取出的那个数插入到左边的合适位置
}
}
希尔排序(Shell Sort)
希尔排序:是插入排序的一种,又称缩小增量排序,是直接插入排序的一种更高效的改进版本。希尔排序是基于插入排序的以下两点性质而提出改进方法的:
-
插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率
-
但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位
希尔排序与插入排序的不同之处在于,它会通过设定带间隔的分组,优先比较距离较远的元素(同时破坏了稳定性)
算法思路:定义一个间隔序列来表示排序过程中进行比较的元素之间有多远的间隔,每次将具有相同间隔的数分为一组,进行插入排序,最后一个分组组数必须是1,当最后一组执行完插入排序,排序结束
时间复杂度:O(n^1.3)
空间复杂度:O(1)
稳定性:不稳定
希尔排序示意图:
C代码:
static void Shell(int *arr, int len,int gap)//每一个分组的排序 ,gap是分组的间隔
{
int tmp;
int i;
int j;
for (i = gap; i < len; i++)
{
tmp = arr[i];
for (j = i - gap; j >= 0; j-=gap)
{
if (tmp >= arr[j])
{
break;
}
else
{
arr[j + gap] = arr[j];
}
}
arr[j + gap] = tmp;
}
}
void ShellSort(int *arr, int len) //希尔排序
{
int d[] = {
5,3,1 };//设定分组的序列
for (int i = 0; i < sizeof(d) / sizeof(d[0]); i++)//按设定的分组间隔依次分组排序
{
Shell(arr, len, d[i]);
}
}
补充:关于希尔排序的增量与复杂度的关系
快速排序(Quick Sort)
算法思路:快速排序使用分治的思想,通过一趟排序将待排序列分割成两部分,其中一部分记录的关键字比另一部分记录的关键字小。之后分别对这两部分记录继续进行排序,以达到整个序列有序的目的
- 在待排序数据中选取一个数据作为基准(可选择第一个数据)
- 使用基准数据将剩余的数据分成两部分,左部分(不一定有序)都比基准小,右部分(不一定有序)都比基准大
- 分别再对左部分和右部分(至少有两个数据)进行快速排序(递归)
时间复杂度:O(nlogn),如果数列本来就有序,则快排退化为选择排序,时间复杂度退为O(n^2),快排越有序越慢,因为选好基准数据后,它是从后往前依次找比基准数据小的数据
空间复杂度:O(logn),递归涉及到函数栈的开辟
稳定性:不稳定
快速排序示意图:
(该动图每次把基准与数字进行交换,代码中是用tmp来保存基准,没有交换)
C代码:三个函数
static int Partition(int *arr, int low, int high)//快排的一次过程,把一个基准扣下来,从后往前找比它小的,从前往后找比它大的
static void Quick(int *arr, int low, int high)//QuickSort的中间层马甲
void QuickSort(int *arr, int len)
//快排的一次划分,笔试的重点,low起始下标,high结尾下标
//把一个基准扣下来,从后往前找比它小的,从前往后找比它大的
static int Partition(int *arr, int low, int high)//一次划分:O(n),O(1)
{
int tmp = arr[low];//基准
while (low < high)
{
//从后往前找比基准小的数字
while (low < high && arr[high] >= tmp)
{
high--;
}
if (arr[high]<tmp)//找到比基准小的数字
{
arr[low] =arr[high];//将数字放到前面
}
//从前往后找比基准大的数字
while (low < high&&arr[low] <= tmp)
{
low++;
}
if (arr[low] > tmp)
{
arr[high] = arr[low];//将数字放到后面
}
}
arr[low] = tmp;//基准应该放的位置
return low;//返回基准放好的位置
}
//QuickSort的中间层马甲
static void Quick(int *arr, int low, int high)//递归次数为O(logn)
{
int mid = Partition(arr, low, high);
if (mid - low > 1)//左边的数据超过一个就要继续排序
{
Quick(arr, low, mid - 1);
}
if (high - mid > 1)//右边的数据超过一个就要继续排序
{
Quick(arr, mid+1, high);
}
}
//封装快排接口
void QuickSort(int *arr, int len)//O(n*logn),O(logn),不稳定
{
Quick(arr, 0, len - 1);
}
快速排序的非递归实现:只用修改Quick函数即可,用栈将一次快排划分的左右下标保存起来,非递归的时间复杂度与空间复杂度同递归实现的数量级是一样的,但是在同样的数据量下,非递归实现对空间消耗的绝对值较小,非递归栈只保存两个下标,而递归实现涉及的是函数栈
快排非递归C代码:
static int Partition(int *arr, int low, int high)//一次划分:O(n),O(1)
{
int tmp = arr[low];//基准
while (low < high)
{
//从后往前找比基准小的数字
while (low < high && arr[high] >= tmp)
{
high--;
}
if (arr[high]<tmp)//找到比基准小的数字
{
arr[low] =arr[high];//将数字放到前面
}
//从前往后找比基准大的数字
while (low < high&&arr[low] <= tmp)
{
low++;
}
if (arr[low] > tmp)
{
arr[high] = arr[low];//将数字放到后面
}
}
arr[low] = tmp;//基准应该放的位置
return low;//返回基准放好的位置
}
//快排的非递归实现--------------------------------------------------------
static void Quick2(int *arr, int low, int high)
{
SqStack st; //栈用来保存每次快排的左下标与右下标
InitStack(&st);
Push(&st, low);
Push(&st, high);
while (!IsEmpty(&st))
{
int right;
Pop(&st, &right);//把high给right
int left;
Pop(&st, &left);
int mid = Partition(arr, left, right);//mid就是一次快排返回的基准位置 left......mid......right
if (mid - left > 1)
{
Push(&st, left);
Push(&st, mid - 1);
}
if (right - mid > 1)
{
Push(&st, mid + 1);
Push(&st, right);
}
}
Destory(&st);
}
//----------------------------------------------------------------------
//封装快排接口
void QuickSort(int *arr, int len)//O(n*logn),O(logn),不稳定
{
Quick2(arr, 0, len - 1);
}
快速排序的优化
对于分治算法,当每次划分时,算法若都能分成两个等长的子序列时,那么分治算法效率会达到最大。也就是说,基准的选择是很重要的。选择基准的方式决定了两个分割后两个子序列的长度,进而对整个算法的效率产生决定性影响。
- 选择基准的方式:
- 固定位置,取序列的第一个或最后一个元素作为基准
- 随机选取基准
- 三位数取中,选取第一个、中间一个、最后一个数据排序取三者的中位数作为基准
快排对于越有序的数列,其时间复杂度和空间复杂度都会增大,基于此可以进行优化。
- 优化一: 当待排序序列的长度分割到一定大小后,使用插入排序。对于很小和部分有序的数组,快排不如插排好。当待排序序列的长度分割到一定大小后,继续分割的效率比插入排序要差,此时可以使用插排而不是快排
- 优化二:在一次分割结束后,可以把与Key相等的元素聚在一起,继续下次分割时,不用再对与key相等元素分割
- 优化三:优化递归操作
- 优化四:使用并行或多线程处理子序列
- 效率比较好的组合是:三数取中+插排+聚集相等元素
堆排序(Heap Sort)
预备知识:
- 完全二叉树:若设二叉树的深度为k,除第 k 层外,其它各层 (1~k-1) 的结点数都达到最大个数,第k 层所有的结点都连续集中在最左边,这就是完全二叉树
- 堆:堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆(父结点大于子结点);或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆(父结点小于子结点)
- 大顶堆是一种逻辑结构,按层序遍历可映射为数组:
- 该数组从逻辑上讲就是一个堆结构,我们用简单的公式来描述一下堆的定义就是:
大顶堆:arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2]
小顶堆:arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2]- 对于完全二叉树,已知父结点下标i,则子结点下标2i+1,2i+2;已知子结点下标i,则父结点下标(i-1)/2
算法思路:将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点。将其与末尾元素进行交换,此时末尾就为最大值。然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。如此反复执行,便能得到一个有序序列了
- 建立大顶堆(升序排序)
- 将堆顶元素与末尾元素交换,将最大元素放置在尾端
- 将去掉尾端元素剩下的继续调整为大顶堆,然后再将堆顶元素交换至末尾,再调整
注意:建立大顶堆需要从后往前多次堆调整,而一次堆调整是从上往下比较的
时间复杂度:O(nlogn)
空间复杂度:O(1)
稳定性:不稳定,跳跃式的交换数据
堆排序示意图:
C代码:
//一次堆调整
static void HeapAdjust(int *arr, int start, int end)//start起始下标,end结尾下标,O(logn),O(1)
{
int tmp = arr[start];
int parent = start;//标记父节点下标
for (int i = 2 * start + 1; i <= end; i = 2 * i + 1)//i下一次要到它的左孩子
{
//找左右孩子的较大值
if (i + 1 <= end && arr[i] < arr[i + 1])
{
i++;
}//i变为左右孩子较大值的下标
if (arr[i] > tmp)
{
//arr[(i - 1) / 2] = arr[i];//放到i的父节点
arr[parent] = arr[i];
}
else
{
break;
}
parent = i;//更新下一次i的父节点
}
arr[parent] = tmp;
}
void HeapSort(int *arr, int len)//O(nlogn),O(1),不稳定(父子相互交换数据,父子下标是跳跃式的)
{
//建立大根堆,O(nlogn)
for (int i = (len - 1 - 1) / 2; i >= 0; i--)//len-1最后一个的下标,再减一除以二是它的父节点下标,从后往前多次调整
{
HeapAdjust(arr, i, len - 1);//每一个i都遍历到len-1作为end,因为即使有的没有len-1这个子节点,也不影响,
}
//每次将根和待排序最后的值交换,然后再调整,O(nlogn)
int tmp;
for (int i = 0; i < len - 1; i++)
{
tmp = arr[0];
arr[0] = arr[len - 1 - i];
arr[len - 1 - i] = tmp;
HeapAdjust(arr, 0, len - 2 - i);
}
}
归并排序(Merge Sort)
算法思路:归并排序就是利用分治归并的思想实现的排序方法。它的原理是假设初始序列含有n个记录,则可以看成是n个有序的子序列,每个子序列的长度为1,然后两两归并,得到[n/2]个长度为2或1的有序子序列,再两两归并,…,如此重复,直至得到一个长度为n的有序序列为止,这种排序方法称为二路归并排序
- 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
- 设定两个指针,最初位置分别为两个已经排序序列的起始位置
- 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置,重复该步骤直到某一指针超出序列尾
- 将另一序列剩下的所有元素直接复制到合并序列尾
时间复杂度:O(nlogn)
空间复杂度:O(n)
稳定性:稳定,没有交换数据,归并排序是一种比较占用内存,但却效率高且稳定的排序算法
归并排序示意图:
C代码:
//非递归实现
static void Merge(int *arr, int len, int gap)//gap归并段的长度,O(n),O(n)
{
int low1 = 0;//第一个归并段的起始下标
int high1 = gap - 1;//第一个归并段的结束下标
int low2 = high1+1;//第二个归并段的起始下标
int high2 = low2 + gap < len ? low2 + gap - 1 : len - 1;//第二个归并段的结束下标
int *brr = (int *)malloc(len*sizeof(int));
int i = 0;//brr的下标
while (low2 < len)
{
//两个归并段都有数据就要进行归并
while (low1 <= high1 && low2 <= high2)
{
if (arr[low1] <= arr[low2])
{
brr[i++] = arr[low1++];
}
else
{
brr[i++] = arr[low2++];
}
}
//一个归并段的数据已经完成,另一个还有数据
while (low1 <= high1)
{
brr[i++] = arr[low1++];
}
while (low2 <= high2)
{
brr[i++] = arr[low2++];
}
//进入到下一块的归并
low1 = high2 + 1;
high1 = low1 + gap - 1;
low2 = high1 + 1;
high2 = low2 + gap < len ? low2 + gap - 1 : len - 1;
}
//打单的段
while (low1 < len)
{
brr[i++] = arr[low1++];
}
for (i = 0; i < len; i++)
{
arr[i] = brr[i];
}
free(brr);
}
void MergeSort(int *arr, int len)//O(nlogn),O(n),稳定(没有交换数据)
{
for (int i = 1; i < len; i *= 2)//O(logn)
{
Merge(arr, len, i);
}
}
基数排序(Radix Sort)
算法思路:基数排序是一种非比较型排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较,基数排序属于“分配式排序”,又称“桶子法”(bucket sort)或bin sort,它是透过键值的部份资讯,将要排序的元素分配至某些“桶”中,藉以达到排序的作用(低位优先,多关键字排序)
- 首先取得待排序数列中的最大值,其位数就是进出的趟数
- 根据每个数的低位关键字将数字分配至不同的桶中
- 各个桶依次出尽数据组成新的队列,再根据下一个低位关键字入队列、出队列,如此反复直至所有位均已完毕,数列便有序
时间复杂度:O(d*n),d是待排序数列最大值的位数
空间复杂度:O(n)
稳定性:稳定
基数排序示意图:
C代码:
/// 基数排序需要用到的链式队列 //
typedef struct Node
{
int data;
struct Node *next;
}Node;
typedef struct HNode
{
Node *front;//队头
Node *rear;//队尾
}HNode,*Queue;
void InitQueue(Queue q)
{
assert(q != NULL);
q->front = NULL;
q->rear = NULL;
}
bool IsEmpty(Queue q)
{
return q->front == NULL;
}
bool Push(Queue q, int val)//入队
{
Node *p = (Node*)malloc(sizeof(Node));
p->data = val;
p->next = NULL;
if (IsEmpty(q))
{
q->front = p;
q->rear = p;
}
else
{
q->rear->next = p;
q->rear = p;
}
return true;
}
bool Pop(Queue q, int *rtval)//出队列获取队头的值,且删除队头
{
if (IsEmpty(q))
{
return false;
}
if (rtval != NULL)
{
*rtval = q->front->data;
}
Node *p = q->front;
q->front = p->next;
if (q->front == NULL)//刚才删除的是最后一个结点
{
q->rear = NULL;
}
free(p);
return true;
}
// 基数排序 //
//获取十进制数字的位数
static int GetDigit(int n)
{
int count = 0;
do
{
n /= 10;
count++;
} while (n != 0);
return count;
}
//得到十进制num右数第n位的数字(从0开始),例:(123,0)->3, (123,1)->2, (123,2)->1
static int Key(int num, int n)//得到当前数的关键字(此时该入的对列)
{
for (int i = 0; i < n; i++)//不停的丢个位数字
{
num /= 10;
}
return num % 10;
}
//每一趟的具体进出,利用链式队列
static void Radix(int *arr, int len, int n)//n是十进制右数第几位,0个位,1十位,2百位...
{
HNode qrr[10];//10个队列的队头结点
int k;//需要进的队列的编号/关键字
for (int i = 0; i < 10; i++)//初始化队列
{
InitQueue(&qrr[i]);
}
//入队
for (int i = 0; i < len; i++)
{
k = Key(arr[i], n);//获取该入的队列编号
Push(&qrr[k], arr[i]);//arr[i]放入k号对列
}
//出队,10个队列依次全出
int i = 0;//arr下标,出的数据要放入arr中去
for (int j = 0; j < 10; j++)
{
while (!IsEmpty(&qrr[j]))//只要当前队列不为空,就要一直出数据
{
Pop(&qrr[j], &arr[i]);
i++;
}
}
}
//基数排序
void RadixSort(int *arr, int len) //O(d*n),d是最大值的位数,O(n)
{
//首先找到最大值
int max = arr[0];
for (int i = 1; i < len; i++)
{
if (max < arr[i])
{
max = arr[i];
}
}
//得到进队和出队的趟数,即最大值的位数
int n= GetDigit(max);
//进出n趟
for (int i = 0; i < n; i++)
{
Radix(arr, len, i);//i是每一趟进出的关键字
}
}
基数排序以上
总结
排序算法 | 平均时间复杂度 | 最坏时间复杂度 | 空间复杂度 | 稳定性 |
---|---|---|---|---|
冒泡排序 | O(n^2) | O(n^2) | O(1) | 稳定 |
选择排序 | O(n^2) | O(n^2) | O(1) | 不稳定 |
插入排序 | O(n^2) | O(n^2) | O(1) | 稳定 |
希尔排序 | O(n^1.3) | O(n^2) | O(1) | 不稳定 |
快速排序 | O(nlogn) | O(n^2) | O(logn) | 不稳定 |
堆排序 | O(nlogn) | O(nlogn) | O(1) | 不稳定 |
归并排序 | O(nlogn) | O(nlogn) | O(n) | 稳定 |
基数排序 | O(d*n) | O(d*n) | O(n) | 稳定 |