排序算法
文章目录
1.数据结构定义
#define MAXSIZE 10000 /* 用于要排序数组个数最大值,可根据需要修改 */
typedef struct
{
int r[MAXSIZE+1]; /* 用于存储要排序数组,r[0]用作哨兵或临时变量 */
int length; /* 用于记录顺序表的长度 */
}SqList;
/* 交换L中数组r的下标为i和j的值 */
void swap(SqList *L,int i,int j)
{
int temp=L->r[i];
L->r[i]=L->r[j];
L->r[j]=temp;
}
2.冒泡排序
平均时间复杂度:O(n2)
从后往前依次比较相邻的两个数,如果后面的数比较小就交换位置,这样的话每循环一次就会有一个最小值被换到最前面了,然后再依次把其他的数冒上去。
go语言实现:
// BubbleSort 冒泡排序
func BubbleSort(array []int) {
length := len(array)
for i := 0; i < length-1; i++ {
for j := length - 1; j > i; j-- {
if array[j] < array[j-1] {
swap(array, j, j-1)
}
}
}
}
func swap(array []int, i, j int) {
temp := array[i]
array[i] = array[j]
array[j] = temp
}
3.冒泡排序的改进
设置一个flag,如果这一轮没有发生交换,就break。
go语言实现:
// FlagBubbleSort 冒泡排序的改进,设置一个flag,如果这一轮没有发生交换,就break
func FlagBubbleSort(array []int) {
length := len(array)
for i := 0; i < length-1; i++ {
flag := false // 表示是否发生交换
for j := length - 1; j > i; j-- {
if array[j] < array[j-1] {
swap(array, j, j-1)
flag = true
}
}
if !flag {
break
}
}
}
4.简单选择排序
平均时间复杂度:O(n2)
在长度为N的无序数组中,
第一次遍历n-1个数,找到最小的数值与第一个元素交换;
第二次遍历n-2个数,找到最小的数值与第二个元素交换;
。。。
第n-1次遍历,找到最小的数值与第n-1个元素交换,排序完成。
go语言实现:
// SimpleSelectSort 简单选择排序
func SimpleSelectSort(array []int) {
length := len(array)
for i := 0; i < length-1; i++ {
minIdx := i
for j := i + 1; j < length; j++ {
if array[j] < array[minIdx] {
minIdx = j
}
}
if minIdx != i {
swap(array, i, minIdx)
}
}
}
func swap(array []int, i, j int) {
temp := array[i]
array[i] = array[j]
array[j] = temp
}
5.直接插入排序
平均时间复杂度:O(n2)
小规模数据或者数据基本有序的时候效果好。
算法流程:
数组下标为0的位置作为哨兵位置(即暂存位置,临时变量)i从第二个数据的位置开始向后遍历,如果L[i]<L[i-1],则说明L[i]需要插入到前面的某个位置,先将L[i]暂存到L[0]的位置,然后让j从i-1的位置依次向前判断,如果L[J]比L[0]大,则将L[j]向后移动一位, 直到找到比L[0]小的位置,把L[0]插入到这个位置的后面。
对于少量数据效果比较好。
另外一种比较好的解释方法,在要排序的一组数中,假定前n-1个数已经排好序,现在将第n个数插到前面的有序数列中(插入位置后面的元素依次向后移动),使得这n个数也是排好顺序的。如此反复循环,直到全部排好顺序。
代码:
/* 对顺序表L作直接插入排序,注意这里实际数据从下标为1的位置开始,这里巧妙利用了下标为0的哨兵,如果不能把下标为0的位置作为哨兵的话需要看下面的Java代码的写法 */
void InsertSort(SqList *L)
{
int i,j;
for(i=2;i<=L->length;i++)
{
if (L->r[i]<L->r[i-1]) /* 需将L->r[i]插入有序子表 */
{
L->r[0]=L->r[i]; /* 设置哨兵 */
for(j=i-1;L->r[j]>L->r[0];j--)
L->r[j+1]=L->r[j]; /* 记录后移 */
L->r[j+1]=L->r[0]; /* 插入到正确位置 */
}
}
}
下面是另一种写法,Java写的:
public static void insert_sort(int array[],int lenth){
int temp;
for(int i=0;i<lenth-1;i++){
for(int j=i+1;j>0;j--){
if(array[j] < array[j-1]){
temp = array[j-1];
array[j-1] = array[j];
array[j] = temp;
}else{ //不需要交换
break;
}
}
}
}
我自己用go写的,简洁一点:
// InsertSort 插入排序
func InsertSort(array []int) {
length := len(array)
for i := 1; i < length; i++ {
for j := i - 1; j >= 0 && array[j] > array[j+1]; j-- {
temp := array[j+1]
array[j+1] = array[j]
array[j] = temp
}
}
}
6.希尔排序(相当于直接插入排序的升级,先搞懂插入排序)
这里讲的比较容易理解,希尔排序-简单易懂图解
初始增量为数组长度的一半,每次排序后变为原来的一半,增量为1的时候停止。 。
拉长了插入排序比较的距离,将数据按增量分成不同的组(见下面的图),然后每组内使用插入排序,当增量为1时排序完毕。
我自己写的go代码:
// ShellSort 希尔排序
func ShellSort(array []int) {
length := len(array)
gap := length
for {
gap = gap >> 1
for i := 0; i < gap; i++ { // 这个循环控制每一组进行插入排序
for j := i + gap; j < length; j += gap { //插入排序算法的外层循环
for k := j - gap; k >= 0 && array[k] > array[k+gap]; k -= gap { // 依次往前交换,直到换到正确的位置
temp := array[k+gap]
array[k+gap] = array[k]
array[k] = temp
}
}
}
if gap == 1 {
break
}
}
}
7.堆排序(相当于简单选择排序的升级)
平均时间复杂度:O(NlogN)
堆排序不适合待排序序列个数较少的情况,因为初始构建堆所需的比较次数较多。
堆排序是借助于完全二叉树的性质,完全二叉树的定义:
对于深度为K的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时称之为完全二叉树。
即除了最后一层以外都是满的,并且最后一层左对齐。如果一个结点不是满的,那么要么没有孩子,要么只有左孩子。
完全二叉树存储在数组中时,是按层从左到右的顺序存储的,因此结点i
的左右子结点的下标分别为2*i+1
和2*i+2
,结点i
的父结点下标为(i-1)/2
,前提是下标从0开始。
主要分为两步:
- 构建堆;
- 堆排序。
构建最大堆
要将数组构建成最大堆的话,只需要从最后一个有子结点的结点(或者说最后一个结点的父结点)开始往上调整,如果数组长度为n
,那么最后一个结点的父结点下标为(n-1-1)/2
,即(n-2)/2
(或者写成n/2-1
),因此只需要从(n-2)/2
向前遍历,每次调用堆调整的函数即可,该函数对当前结点及后面的结点进行遍历,找出左右子结点的最大值,看父结点的值是否比子结点的最大值更小,如果是的话就交换,然后对与其交换的子结点继续按上面的方式调整(因为父节点换到下面以后可能会破坏子结点的堆结构),如果当前父结点比两个子结点都大就打破循环,不再调整下层(因为下层是调整过的,如果父结点比两个子结点都大,说明不需要调整)。
堆排序
将数组构建成最大堆后,堆顶(数组开头的元素)就是最大值,这时候把最大值和最后以为互换(最大值排序完毕),再对最后一个数之前的序列进行堆调整,然后依次将第二大、第三大…的值换到后面,就完成了堆排序。
如下图所示,分别对下标为4,3,2,1,0的结点调用堆调整的函数使其在整棵树中满足最大堆的定义,然后依次将最大值换到最后面,再对前面的数组重新进行堆调整。
go代码:
// HeapSort 堆排序
func HeapSort(array []int) {
length := len(array)
if length <= 1 {
return
}
// 构建最大堆
MakeMaxHeap(array, 0, length-1)
// 堆排序,将最大值放到最后,再对前面的重新进行堆调整
for i := length - 1; i > 0; i-- {
temp := array[0]
array[0] = array[i]
array[i] = temp
MaxHeapAdjust(array, 0, i-1)
}
}
// MakeMaxHeap 构建最大堆,如果把里面的maxHeapAdjust函数换成最小堆调整的函数,就成了构建最小堆
func MakeMaxHeap(array []int, start, end int) {
length := end - start + 1
if length <= 1 {
return
}
// 从最后一个父结点往前依次调整,使其成为最大堆
// 因为刚开始数组是乱的,因此要从最后一个父结点依次向上调整,逐渐构建大顶堆
// 最后一个结点的下标为length-1,结点i的父结点下标为(i-1)/2
for i := start + (length-2)/2; i >= start; i-- {
MaxHeapAdjust(array, i, end)
}
}
// MaxHeapAdjust 最大堆调整,start结点及其子结点调整成满足最大堆的要求
// 前提是除了根结点start以外,下面的部分已经调整成最大堆了
// start可以看成parent,表示将一整棵完全二叉树中的parent结点及其所有子结点进行调整,使其在树中满足最大堆的要求
// 注意不是把这个区间看成一个单独的完全二叉树来调整,而是这个区间内的结点是整个array数组构成的完全二叉树中的一部分(这一句看不懂没关系,不用管)
func MaxHeapAdjust(array []int, start, end int) {
parent := start
child := 2*parent + 1 // 左子结点,完全二叉树中结点i的左子结点下标为2*i+1,
for child <= end {
// 找出子结点的最大值,如果父结点比最大的子结点要小,则需要调整
if child+1 <= end && array[child] < array[child+1] {
child++
}
// 如果子结点的最大值比父结点大,则不需要调整
if array[child] < array[parent] { //构建最小堆的话只需要把这里和上面的小于号改成大于号即可
break
}
temp := array[parent]
array[parent] = array[child]
array[child] = temp
// 交换以后子结点及其子树的堆结构可能被破坏,因此需要继续调整
parent = child
child = 2*parent + 1
}
}
8.归并排序
归并排序表示将两个或两个以上的有序表组合成一个新的有序表。
使用归并排序的时候应该尽量考虑迭代的方法,而不是递归的方法,因为递归的方法占用空间多,而且在时间上也不如迭代的方法。
各种情况下时间复杂度都是一样的,且是稳定排序。但是需要创建临时数组,这个数组和原始数组一样大,递归的时候一直使用这个作为临时数组。空间换时间。
将两段有序的序列合并的时候,使用的方法是依次比较,哪个小就放到临时数组里面,合并完以后再复制回去。
我自己写的go语言版:
// MergeSort 归并排序主函数
func MergeSort(array []int) {
length := len(array)
temp := make([]int, length)
mergeSort(array, 0, length-1, temp)
}
// mergeSort 归并排序
func mergeSort(array []int, first, end int, temp []int) {
if first == end {
return
}
middle := (first + end) / 2
mergeSort(array, first, middle, temp)
mergeSort(array, middle+1, end, temp)
mergeArray(array, first, middle, end, temp)
}
// mergeArray 从middle将first到end的序列分成两部分,将这两部分合并到temp序列,然后再拷贝回原序列,这里也可以改成合并的时候temp只使用first到end这一段,这样左右两边就可以并行操作
func mergeArray(array []int, first, middle, end int, temp []int) {
i := first
j := middle + 1
k := 0
// 两个有序序列中,谁的数字小就把谁的数字放入临时序列
for i <= middle && j <= end {
if array[i] < array[j] {
temp[k] = array[i]
k++
i++
} else {
temp[k] = array[j]
k++
j++
}
}
// 两个序列长度不一样的话,把长的那个序列剩下的数字直接复制到temp序列
for i <= middle {
temp[k] = array[i]
k++
i++
}
for j <= end {
temp[k] = array[j]
k++
j++
}
// 将temp序列的内容复制回原序列
for i = 0; i < k; i++ {
array[first+i] = temp[i]
}
}
9.快速排序(冒泡排序的升级)
平均时间复杂度:O(N*logN)
先选取一个数作为枢轴(直接选取第一个数,三数取中法,九数取中法),然后对序列进行调整,使得比枢轴小的数都在枢轴的左边,比枢轴大的数在枢轴右边,再对枢轴左边和右边的序列递归进行快速排序。
该方法的基本思想是:
1.先从数列中取出一个数作为基准数。
2.分区过程,将比这个数大的数全放到它的右边,小于或等于它的数全放到它的左边。
3.再对左右区间重复第二步,直到各区间只有一个数。
简单的说就是挖坑填数,快速排序法的思想是,先选一个数最为枢轴x
(一般要求不高的话就选第一个数,变量名也可以叫pivot
,就是枢轴的意思),把这个数x
先单独保存到一个变量里面,这就相当于在第一个位置挖了一个坑,别的数字可以填过来了。然后创建两个指针left
和right
,指针right
从右边往左边找直到有一个比x
小的数,把这个数填到坑的位置,然后就获得了一个新坑,再从左往右找直到有一个比x
大的数,再把这个数填到坑里面,又获得了一个新坑,然后指针left
和right
继续按上面的方法往中间找,直到两个指针相遇,这时候坑的左边就全都是比x
小的树,右边就全都是比x
大的数,然后把x
填到坑里面,接着使用递归,分别对坑左边和右边的序列使用相同的方法进行排序即可。
go语言写的快速排序:
func quickSort(array []int, l int, r int) {
if l >= r || len(array) <= 1 {
return
}
left, right := l, r
pivot := array[left] // 保存枢轴数字,此时left指针指向坑
for left < right {
// 从右往左找到一个比枢轴pivot小的数
for right > left && array[right] >= pivot {
right--
}
array[left] = array[right] // 此时right指针的位置变成了新的坑
for left < right && array[left] <= pivot {
left++
}
array[right] = array[left] // 此时left指针变成了新的坑
}
array[left] = pivot // 用枢轴把坑填上,此时left指针和right指针已经相遇
quickSort(array, l, left-1)
quickSort(array, right+1, r)
}
C++分区过程算法,加上递归和返回条件就是快速排序了,一些其他算法会用到这个分区算法(比如求数组的前k个最小的数)
int Partition(vector<int> &input, int left, int right){
int pivot = input[left];
while(left<right){
while(right>left && input[right]>=pivot){
right--;
}
input[left] = input[right];
while(left<right && input[left]<=pivot){
left++;
}
input[right] = input[left];
}
input[left] = pivot;
return left;
}
参考
《大话数据结构》
来源:CSDN
作者:026后勤仓库保管员
链接:https://blog.csdn.net/Dragon_Prince/article/details/104215195