排序算法

社会主义新天地 提交于 2020-02-08 05:13:18

排序算法

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+12*i+2,结点i的父结点下标为(i-1)/2,前提是下标从0开始。

主要分为两步:

  1. 构建堆;
  2. 堆排序。

构建最大堆
要将数组构建成最大堆的话,只需要从最后一个有子结点的结点(或者说最后一个结点的父结点)开始往上调整,如果数组长度为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先单独保存到一个变量里面,这就相当于在第一个位置挖了一个坑,别的数字可以填过来了。然后创建两个指针leftright,指针right从右边往左边找直到有一个比x小的数,把这个数填到坑的位置,然后就获得了一个新坑,再从左往右找直到有一个比x大的数,再把这个数填到坑里面,又获得了一个新坑,然后指针leftright继续按上面的方法往中间找,直到两个指针相遇,这时候坑的左边就全都是比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;
}

参考

《大话数据结构》

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!