常用排序算法整理(C语言python语言实现)

排序算法

排序基本概念与分类

  将数据集进行排序,如果数据本身不可以排序,可以使用hash函数建立一个顺序列,映射到整数集,然后再进行排序。

  1. 内排序:待排序的数据全部存储在内存当中,很多排序算法都是内排序
  2. 外排序:存储在磁带和磁盘的数据叫做外排序,归并排序是外排的基础。
  3. 原地排序算法:不需要额外的空间,只需要几个额外的中间变量。
  4. 时间复杂度:最优就是O(nlogn),最差就是O(n2),
  5. 稳定性:相同的数据元素排序前后的位置是不改变,这样的叫做稳定排序,而前后顺序发生了改变就是不稳定的。
  6. 适应性:如果一个排序算法对接近有序的序列工作的更快,就称这种算法具有适用性。

1. 插入排序

思路:

  类似与扑克牌摸牌,将一个记录插入到已经排好序的有序表中,从而得到一个新的有序表。

for 第二个元素 to 最后一个元素:
	while 插入元素 > 前一个元素 and 前一个元素不为空:
			前一个元素后移
	将该元素插入到当前位置

代码:

// C
void InsertSort(int *nums, int len)
{
    int i, j;
    int temp;
    for (i = 1; i < len; i++) {
        temp = nums[i];
        j = i;
        while (j > 0 && nums[j-1] > temp) {
            nums[j] = nums[j-1];
            j--;
        }
        nums[j] = temp;
    }
}
#python
def insert_method(lst):
    for i in range(1, len(lst)):
        j = i
        x = lst[i]
        while j > 0 and lst[j-1] > x:
            lst[j] = lst[j-1]
            j -= 1
        lst[j] = x
    return lst

特点:

  最好时间复杂度为O(n),情况发生在数组本来就是从小到大排列。

项目最坏最好平均空间复杂度稳定性适应性
插入排序n2nn21稳定

2. 选择排序

思路:

  找到合适的关键字再做交换,并且只移动一次就完成,每次循环都要找到最小的元素放到合适的位置。

for 第一个元素(i) to 倒数第二个元素:
	for 第二个元素 to 最后一个元素:
		记录最小的元素值
	最小值赋值给i

代码:

// C
void SelectSort(int *nums, int len)
{
    int i, j, x;
    int temp;
    for (i = 0; i < len; i++) {
        x = i;
        for (j = i + 1; j < len; j++) {
            if (nums[j] < nums[x]) {
                x = j;
            }
        }
        if (x != i) {
            temp = nums[i];
            nums[i] = nums[x];
            nums[x] = temp;
        }
    }
}
#python
def select_method(lst):
    for i in range(0, len(lst)-1):
        x = i
        for j in range(i+1, len(lst)):
            if lst[j] < lst[x]:
                x = j
        if x != i:
            lst[i], lst[x] = lst[x], lst[i]
    return lst

特点:

  此算法的平均以及最坏时间复杂度都是o(n^2),每一次循环都要从i到尾进行比较。所以算法没有适应性。从头到尾扫描最小值,这样相同关键码的值肯定要是不会破坏顺序的。但是如果是3,3,1。这样的,一和三交换就破环了稳定性,所以算法是不稳定的

项目最坏最好平均空间复杂度稳定性适应性
插入排序n2n2n21不稳定

3. 希尔排序

思路:

  该排序方式是第一批突破O(n2)时间复杂度的算法之一,中心思想是将待排序数组,分为几个子序列,保证子序列之间大致是按照从小到大的趋势。拆分的方法是选取一个跨度值gap,按照相同跨度值元素归为一个子序列,如{9,1,5,8,3,7,4}选取跨度为2时,则分为{9,5,3,4}、{1,8,7}两组,然后两组分别采用插入排序的方法保证子序列有序,然后逐步减小跨度gap的值,直到为1最后一次排序,由于数组已经接近有序,所以插入排序就很高效。

	while 跨度gap > 0:
		for 下标为gap的元素 to 最后一个元素:	
			插入排序的算法保证跨度gap的子序列有序
		gaps 缩小 2

代码:

// c
void ShellSort(int *nums, int len)
{
    int gap, i, j , temp;
    gap = len / 2;
    while (gap > 0) {
        for (i = gap; i < len; i++) {
            if (nums[i-gap] > nums[i]) {
                temp = nums[i];
                for (j = i-gap; j >= 0 && nums[j] > temp; j -= gap) {
                    nums[j+gap] = nums[j];
                }
                nums[j+gap] = temp;
            }
        }
        gap /= 2;
    }
}
#python
def shell_sort(lst):
    n = len(lst)
    gap = n//2
    while gap > 0:
        for i in range(gap, n):
            j = i
            while j - gap >= 0 and lst[j-gap] > lst[j]:
                lst[j-gap], t[j] = lst[j], lst[j-gap]
                j -= gap
        gap = gap // 2
    return lst

特点:

  该算法的排序是基于gap值的选取,gap为一个增量序列,因此其时间复杂度为O(n3/2),比通常的O(n2)要好,但是必须注意,增量序列的最后一个值必须为1,并且由于排序是跳跃式的,因此不是稳定排序。

)项目最坏最好平均空间复杂度稳定性适应性
希尔排序n2nn3/21不稳定

4. 堆排序

思路:

  将待排序的序列构造成一个大堆顶,每次将大堆顶的第一个最大元素取出,存到序列的最后一位,然后再把前n-1个元素重新构造成大堆顶。
  该过过程需要两个步骤:

	 1. 将序列的构造成一个大堆顶;
	 2. 每次取出顶部元素以后,重新恢复大堆顶。

代码:

void siftdown(int *nums, int begin, int end)
{
    int i, j, temp;
    i = begin;
    j = 2 * i + 1;
    temp = nums[i];
    while (j < end) {
        if ((j + 1) < end && nums[j+1] > nums[j]) {
            j = j + 1;    
        }
        if (nums[j] < temp) {
            break;
        }
        nums[i] = nums[j];
        i = j;
        j = 2 * j + 1;
    }
    nums[i] = temp;
}

void HeapSort(int *nums, int len)
{
    int i, temp;
    for (i = len / 2; i >= 0; i--) {
        siftdown(nums, i, len);
    }
    for (i = len-1; i >= 0; i--) {
        temp = nums[i];
        nums[i] = nums[0];
        nums[0] = temp;
        siftdown(nums, 0, i);
    }
}

#python
def heap_method(lst):
    def siftdown(lst, e, begin, end):
        i, j = begin, begin*2+1
        while j < end:
            if j+1 < end and lst[j+1] < lst[j]:
                j += 1
            if lst[j] > e:
                break
            lst[i] = lst[j]
            i, j = j, 2 * j + 1
        lst[i] = e

    end = len(lst)
    for i in range(end//2-1, -1, -1):
        siftdown(lst, lst[i], i, end)
    for i in range(end-1, 0, -1):
        e = lst[i]
        lst[i] = lst[0]
        siftdown(lst, e, 0, i)
    return lst

特点:

  该算法的排序主要消耗是初始化建堆时和重构堆的反复筛选上,在建堆的时候,是从最下层最右端的元素开始,外层需要循环n次,然后进入筛选函数中,因为已经保证底层是大堆顶了,因此函数内部只需要判断一次,和左右节点判断选出最大的元素即可,因次筛选函数内部只执行O(1)次,整体构建时需要O(n)的时间复杂度。
  在正式排序时,第i次取堆顶记录重建大堆顶需要O(logn)次,外层
一共需要循环n次,因此排序时间复杂对为O(nlogn)。

项目最坏最好平均空间复杂度稳定性适应性
堆排序nlognnlognnlogn1不稳定

从该排序方法开始,都是根据消除逆序的思维来进行排序,即为交换排序的思维。

5. 冒泡排序

思路:

  通过两两比较的方法,把最大的元素依次交换到最后的位置,接着依次循环把大的值往后交换,可以在一定程度上优化排序算法的复杂度。

代码:

void BubbleSort(int *nums, int len)
{
    int i, j, temp;
    int find;
    for (i = 0; i < len - 1; i++) {
        find = 0;
        for (j = 0; j < len - i - 1; j++) {
            if (nums[j] > nums[j+1]) {
                temp = nums[j];
                nums[j] = nums[j+1];
                nums[j+1] = temp;
                find = 1;
            }
        }
        if (find == 0) {
            break;
        }
    }
}
#python
def bubble_method(lst):
    for i in range(len(lst)-1):
        found = False
        for j in range(len(lst)-1-i):
            if lst[j] > lst[j+1]:
                lst[j], lst[j+1] = lst[j+1], lst[j]
                found = True
        if not found:
            break
    return lst

特点:

  该排序的方法,是两两元素之间进行比较,每一次循环,如果没有遍历一遍没有发现存在逆序元素,则代表序列已经是有序的了,则这时通过标志位跳出循环,可以降低算法的时间复杂度,在序列已经是排好序时,则效率最快,序列为倒序要排位正序时,效率最慢。

项目最坏最好平均空间复杂度稳定性适应性
冒泡排序n2nn21稳定

6. 快速排序

思路:

  选取第一个元素作为中间元素,将待排序的序列分割成为独立的两部分,中间元素左侧的所有值要小,中间元素右侧值都要大于中间值,接着分别对两部分进行排序,只需要细分logn次,内部比较n次,即可完成排序。算法整体采用递归的思想。

代码:

void QuickSortCore(int * nums, int begin, int end)
{
    int i, j, temp;
    i = begin; 
    j = end;
    if (i >= j) {
        return;
    } 
    temp = nums[i];
    while (i < j) {
        while ((i < j) && (nums[j] >= temp)) {
            j--;
        }
        if (i < j) {
            nums[i] = nums[j];
            i++;
        }
        while ((i < j) && (nums[i] <= temp)) {
            i++;
        }
        if (i < j) {
            nums[j] = nums[i];
            j--;
        }
    }
    nums[i] = temp;

    QuickSortCore(nums, begin, i-1);
    QuickSortCore(nums, i+1, end);
}
void QuickSort(int *nums, int len)
{
    QuickSortCore(nums, 0, len - 1);
}

def quick_sort(lst):
    def qsort_method(lst, begin, end):
        if begin >= end:
            return
        i = begin
        j = end
        dummy = lst[i]
        while i < j:
            while i < j and lst[j] > dummy:
                j -= 1
            if i < j:
                lst[i] = lst[j]
                i += 1
            while i < j and lst[i] < dummy:
                i += 1
            if i < j:
                lst[j] = lst[i]
                j -= 1
        lst[i] = dummy
        qsort_method(lst, begin, i-1)
        qsort_method(lst, i+1, end)

    qsort_method(lst, 0, len(lst)-1)
    return lst

特点:

  抽象的看,快速排序产生的划分结构,可以看作以中间元素为根,两边的分别作为左子树和右子树进行再排序,可以类比为左右子树的二叉树,二叉树的平均高度为logn。越是接近排好序的序列,算法效率越低。由于递归情况的存在,会造成栈空间的使用,最好的情况,递归树的高度为logn,最坏情况为O(n),平均为O(logn)。算法没有适应性,反而越有序的适得其反。同时算法也是不稳定的。

项目最坏最好平均空间复杂度稳定性适应性
快速排序n2nlognnlognlogn~n(递归次数)不稳定

7. 归并排序

思路:

  类似于二叉树的倒置,将数组一直拆分为每一个元素,接着两两排序合并,存放在临时变量数组中,然后将临时数组中的值拷贝到原数组中,如此重复下去,直到所有分成两段的数组都合并为同一个有序数组。

代码:

/* 归并排序 */
void Merge(int *nums, int start, int mid, int end)
{
    int numsTemp[end-start+1];
    int i, j, o = 0;
    i = start;
    j = mid + 1;
    while (i <= mid && j <= end) {
        if (nums[i] > nums[j]) {
            numsTemp[o++] = nums[j++];
        } else {
            numsTemp[o++] = nums[i++];
        }
    }
    while (i <= mid) {
        numsTemp[o++] = nums[i++];
    }
    while (j <= end) {
        numsTemp[o++] = nums[j++];
    }
    for (o = 0; o < end - start + 1; o++) {
        nums[o + start] = numsTemp[o];
    }
}

void MergeSortDiv(int *nums, int start, int end)
{
    int mid;
    if (start < end) {
        mid = (start + end) / 2;
        MergeSortDiv(nums, start, mid);
        MergeSortDiv(nums, mid+1, end);
        Merge(nums, start, mid, end);
    }
}
void MergeSort(int *nums, int len) 
{
    int start, end;
    start = 0;
    end = len - 1;
    MergeSortDiv(nums, start, end);
}
#python
def merge_sort(lst):
    def merge(lfrom, lto, low, mid, high):
        i, j, k = low, mid, low
        while i < mid and j < high:
            if lfrom[i] <= lfrom[j]:
                lto[k] = lfrom[i]
                i += 1
            else:
                lto[k] = lfrom[j]
                j += 1
            k += 1
        while i < mid:
            lto[k] = lfrom[i]
            i += 1
            k += 1
        while j < high:
            lto[k] = lfrom[j]
            j += 1
            k += 1

    def merge_pass(lfrom, lto, llen, slen):
        i = 0
        while i + 2 * slen < llen:
            merge(lfrom, lto, i, i+slen, i+2*slen)
            i += 2 * slen
        if i +slen < llen:
            merge(lfrom, lto, i, i+slen, llen)
        else:
            for j in range(i, llen):
                lto[j] = lfrom[j]

    slen, llen = 1, len(lst)
    template = [None] * llen
    while slen < llen:
        merge_pass(lst, template, llen, slen)
        slen *= 2
        merge_pass(template, lst, llen, slen)
        slen *= 2
    return lst

特点:

  抽象的看,归并排序类似二叉树的倒置,因此时间复杂度和树的高度有关,每一趟归并,需要将数组从头到尾扫描一遍,时间复杂度为O(n),树的高度为logn,因此时间复杂度为O(nlogn)。由于归并在过程中需要临时变量数组存放元素,并且需要递归深度为logn,因此空间复杂度为O(n+logn),归并是两两比较因此是稳定的,比较栈用内存但是是效率高且稳定的算法。

项目最坏最好平均空间复杂度稳定性适应性
归并排序nlognnlognnlognn+logn稳定

8. 代码示例

项目链接
CGithub
pythonGithub
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值