几种重要的排序算法——交换排序

交换排序

所谓交换,是指根据序列中两个关键字比较的结果来对换这两个关键字在序列中的位置。交换排序本文介绍两种,冒泡排序(bubble sort)和快速排序。

冒泡排序

排序思路

每次比较两个相邻的元素,如果它们的顺序不符合既定的排序规则(本文以从小到大升序为既定正序),即前一个的元素值大于后一个的元素值时交换这两个元素。例: 
我们需要将 11 1 8 5 这四个数从小到大进行排序,即越大的数排在越后边

废话不多说,我们来模拟一下冒泡的过程!

首先比较第一位和第二位的大小,第一位是“11”,第二位是“1”,“11”要比“1”要大,因为我们规定为升序排列,所以它两不符合我们制定的规则,所以交换它两的位置,形成新的序列 1 11 8 5 。继续比较第二位和第三位的,“11”比“8”大,因此交换这两个元素的位置,交换后变成 1 8 11 5 。继而再将第三个元素与第四个元素进行比较,”11“比”5“大,交换,1 8 5 11 。至此,第一轮的遍历已经完成,此时已经有一个元素归位了,即最大的元素到了序列的最后的位置上(如果规定是降序排列,则最小的元素会到达最后的位置上,即最小的元素归位)。

因为第一趟已经将最大的元素归位了,即最大的元素站到了它本该站的位置。是的,没这个最大的元素啥事情了,它不用管其它的元素的状态了,其他未归位的元素也不用再与它比较。“它退休啦”!因此在下次遍历时即只用比较前 n − 1 n-1 n1个元素,在本例中即比较余下的前三个元素 1 8 5 ,剩下了它们三还需要继续““冒泡”的过程,“冒泡”累积经验,达到一定程度就能“退休”。过程类似下图:在这里插入图片描述

第二轮遍历开始,将“1”与“8”进行比较,“8”比“1”大且“8”的位置在“1”的后面,不用交换,1 8 5

移动到下一元素,比较“8”与“5”,交换,1 5 8,至此,因为“8”后面的“11”已经归位了,也就不用再进行比较,所以此时“8”也归位了,还剩下1 5需要进行比较 。

第三轮遍历,“1”和“5” 比较,“5”大,“5”排在“1”后边,两者位置不交换,因为“8”已经归位,所以“5”也不再需要与“8”比较,此时“5”也归位了。

最后只剩下“1”,只有一个元素,所以也就不用再进行“比较”,“交换”的过程了,整体排序完成。

整体动画演示如下(动画演示非本文所模拟的例子)
在这里插入图片描述

冒泡排序的思路不难,总结起来就两步:

  • 相邻元素进行比较
  • 按指定规则进行交换

c/c++代码描述如下:

#include <cstdio>
#include <cstdlib>
#include <iostream>
#define MaxSize 50

template <typename Ele>
void swap1(Ele &a, Ele &b, Ele &c)
{
	c = a;
	a = b;
	b = c;
}

void bubblesort(int *a, int n)
{
	bool flag = false;
	for (int i = n; i >= 2; i--) // 总共需要进行n-1轮的遍历
	{
		flag = 0;					 // 这儿设置了一个标记,当遍历一趟后没有发生元素的交换则说明
									 // 整体已经是呈升序(或降序)
		for (int j = 2; j <= i; j++) // 因为每轮都有一个元素归位了,
									 // 所以每次都不用再与最后一个元素进行比较,
									 // 前面的循环中已经帮我们做好了这个事情,i每次遍历减一
		{
			if (a[j] < a[j - 1])
			{
				swap1(a[j], a[j - 1], a[0]); // 交换,其实也没必要我这样写,
											 // 我这样增加了代码的复杂度,可读性不高,不是好代码
				flag = 1;					 // 发生了交换,将flag设置为1
			}
		}
		if (!flag) // 没有发生交换,退出循环
			break;
	}
	for (int k = 1; k <= n; k++)
		std::cout << a[k] << '\t';
	std::cout << std::endl;
}

int main()
{
	int n;
	std::cin >> n;
	int a[MaxSize];
	for (int i = 1; i < n + 1; i++)
		std::cin >> a[i];
	bubblesort(a, 15);
	system("pause");
	return 0;
}

python代码描述如下:

def bubble_sort(a):         # 假定按照升序排列
    n = len(a)
    for i in range(n-1, 1, -1):
        flag = False        # 当遍历完一趟没有发生交换,说明序列已有序 退出
        for j in range(1, i):
            if a[j] < a[j - 1]:     # 如果后边的数比前边的数小,交换
                tmp = a[j]
                a[j] = a[j - 1]
                a[j - 1] = tmp
                flag = True
        if not flag:
            break
    print(a)


if __name__ == '__main__':
    a = [3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48]
    bubble_sort(a)

算法分析

空间复杂度:因为冒泡排序并未借助其他多余的空间,只是在原地进行排序,所以时间复杂度为 O ( 1 ) O(1) O(1)

时间复杂度:每进行的一轮的冒泡每个元素都需要与相邻的元素进行比较,而这个过程要经历n轮,所以不难看出冒泡排序的时间复杂度为 O ( n 2 ) O(n^2) O(n2)

稳定性:冒泡排序在元素交换的过程中,相等的两个元素的值的相对位置不会发生变化,所以冒泡排序是一种稳定的排序算法。

快速排序

既然是放在交换排序的大类中,那么快排的核心步骤肯定也是交换。

排序思路

我们这里以升序来进行思路算法描述。在给定的序列中任取一个位置的元素作为基准元素(该元素的位置可以是随机生成,也可以取序列中间位置亦或是序列头和序列尾的位置)。设置两个哨兵,分别放置在序列的尾部与头部(咱们暂且把题目叫做“头哨”、“尾哨”),首先由尾哨开始递减,找到第一个比基准元素小的元素值,继而头哨递增,找到第一个比基准元素大的元素,这时把头哨、尾哨指向的值进行交换。接着再由尾哨递减,继续找比基准元素小的值,找到后又停下来,头哨又开始递增找比基准元素大的值……

两个哨兵一直进行到相遇的时候即停下来,这时在它们相遇的位置的左边的元素值都比基准元素小,在它们右边的元素的值都比基准元素大,这时候我们就把基准元素放到它们相遇的这个位置上,一轮的排序也就完成了。

接着再递归调用处理当前哨兵相遇位置两边的两个子序列,递归调用代码如下

quicksort(left, i-1)、quicksort(i+1, right)

当递归调用完成时,整个序列的排序也就完成了。

问:上面的思路描述你看懂了么?没看懂就对了,因为我说的不是人话(要是有人看懂了,我当场开直播把奥里给嚼碎了吞下去)。
在这里插入图片描述

下面我来讲讲人话,大家都能看懂的人话。

在给定序列中,以一个元素为旗帜(用来标记当前需要归位的元素),我们这里假设是序列中间位置的那个元素(这个值可以是任意的),在序列头和序列尾部站了两个有钱人(就简单的理解是牛A和哈利油这两个人物吧)。牛A在右头,哈利油在左头。他们在完一个游戏,互相朝着对方走过去,脚底下踩着的数字代表着money的多少,哈利油要把他经过的路径上的比旗帜所指的值小的money交换到旗帜的左边,牛A把经过路径上的比旗帜所指值大的money交换到旗帜右边。(不得不说,有钱人真会玩)形象如下图(这里我们按照升序排列)

我们用旗帜来标记我们选取的基准数,这里取序列的中间值,此时为初始状态,旗帜底下指向的元素值是3。快排的主要步骤便是把比旗帜底下小的元素都放置到旗帜所指元素的左边,把比旗帜底下元素大的元素放置到旗帜所指元素的右边,当牛A和哈利油相遇的时候,他们两的左边的元素值均比旗帜所指的元素值小,右边元素值均比旗帜所指的元素值大。

他们的游戏规则如下:

  • 首先由哈利油开始出发(绿头发),方向是朝着牛A的方向,每次只能走一步,每走一步需要检查一下脚底下的金额大小,如果比旗帜所指的值要小,则此时哈利油停下来,轮到牛A的回合,牛A开始前进。
  • 牛A朝着哈利油的方向前进,每次只能走一步每走一步需要检查一下脚底下的金额大小,如果比旗帜所指的值要大,则此时牛A停下来。
  • 当他们都停下来的时候,交换他们此时脚底下的金钱数值,重复上述步骤,直到两人碰面,走到了同一个金额数上方,此时便将旗帜所指的值放到他们两碰面的位置(他们朝着对方前进的过程中都是和旗帜所指的金钱值进行比较的,所以当他们碰面时左边的所以的金钱数值都会比旗帜所指的小,右边的都会要大)。
  • 然后根据他们碰面的位置,将序列重新划分为两个子序列,重复上面的游戏过程直至所有金额按升序排列。

为方便运算,我们将旗帜所指的金额先单独拿出来,用一个宇宙级保险箱给它装着。(即把它放在序列的第一个位置,将原来序列的第一个位置的元素放到旗帜的位置下)
在这里插入图片描述)

首先由哈利油先行动,开着他的法拉利“况且,况且……”轰轰烈烈的朝着牛A出发了,刚走一步,他便检查到他底下的元素值要比旗帜标记的值要小,他知道,只要他发现脚底下的值比旗帜所指的值要小时,他就停下来,然后用他的iphone15给牛A发短信,告诉她“我停下来了,你可以前进啦!”在这里插入图片描述

继而,牛A开始行动,他一开始走一步(走到下标为1的位置)便发现脚底下的金额值比旗帜所指的值大,于是牛A也停了下来,牛A和哈利油互换脚底下的金额值(实际操作是哈利油先将元素值放到牛A的底下,然后牛A再前进,寻找到一个比基准值大的元素值再放到哈利油停下来的位置)。

在这里插入图片描述

交换完成后又到了哈利油的回合,哈利油继续朝牛A前进,一路畅通无阻,直接一个油门就踩到了牛A脸上,两人面基成功(因为在这段序列中没有比旗帜所指元素”3“小的值,所以哈利油可以一直前进)
在这里插入图片描述

当两人碰面时,说明此时两人左侧的money都比基准money要小,右侧都比基准money要大,所以将基准值放置到两人相遇的位置上,即图中金色部分。

之后便是递归的调用,因为右侧被分割后只有一个元素了,所以我们以左侧来模拟。

在这里插入图片描述
基准值取下标为4的元素,进行上一轮同样的操作,将基准金额用一个宇宙无敌安全的保险箱锁起来,牢实到隔壁二哈都咬不开(基准值选取的19,见上图),然后将子序列的第一个元素值放到旗帜原来的位置,即4的位置。在这里插入图片描述

哈利油刚一出发便发现,他脚底下的money要比基准money要小于是他停下来,将他脚底下的money放到牛A的位置,然后通知牛A,发了条语音,“老妹儿,搞快点,到你啦!”。牛A出发,走到下标为4的位置,即元素值为“44”的位置,此时的money比基准money大,于是她把“44”放到哈利油的位置(哈利油停在原地6的位置)交换后如下:

在这里插入图片描述

交换完成后,哈利油又继续前进了,一脚油门又踩到了牛A的脸上(不得不说,哈利油好猛),因为在这个区间上没有比基准money小的元素值,所以哈利油可以直接冲到牛A脸上,然后两人再次面基成功,将基准money当到两人相遇的位置,即下标为3的位置。
在这里插入图片描述

之后问题便分割成了两个子问题,继而将他们两相遇的位置返回给主函数,递归调用quick_sort便可以把整个序列排好了!(返回值即quicksort(left, i-1)、quicksort(i+1, right)中的i值)在这里插入图片描述

快排的每一次的排序都会有一个元素值归位,即每一次排序完成之后都会有一个元素处在它在序列的“正确的”位置上。

本例无动画演示(动画太垃圾了,看看我的漫画吧!)
在这里插入图片描述

c/c++代码描述如下:

#include<cstdio>
#include<iostream>
#include<cstdlib>
#include<algorithm>
#define MaxSize 20

int partition(int * a,int left,int right);
void quick_sort(int * a,int left,int right);

int main(void)
{
	int a[MaxSize] = {3, 44, 38, 5, 47, 15, 36, 26, 27, 3, 46, 4, 19, 50, 48};
	quick_sort(a,0,14);
	for(int i=0;i<15;i++)
		std::cout << a[i] << ' ';
	std::cout << std::endl; 
	system("pause");
	return 0;
}


int partition(int *a,int left,int right)
{
	int mid = left + (right - left) / 2; // 选取中间元素作为旗帜
	int temp = a[mid];
	std::swap(a[mid],a[left]);  // 将基准值放到保险箱,将第一个位置的值放到中间位置
	while(left < right)
	{
		while(left < right && a[right] >= temp) // 找比基准值小的
			right--;
		a[left] = a[right];
		while(left < right && a[left] <= temp)	// 找比基准值大的
			left++;
		a[right] = a[left];
	}
	a[left] = temp;		// 基准值归位
	return left;
}

void quick_sort(int *a,int left,int right)
{
	if(left < right)	// 递归调用
	{
		int mid = partition(a,left,right);
		quick_sort(a,left,mid-1);		// 左半部分排序
		quick_sort(a,mid+1,right);		// 右半部分排序
	}
}

python代码描述如下:

def partiion(a, left, right):
    mid = left + (right - left) // 2  # 求中间值
    temp = a[mid]  # 设置中间元素值为基准值
    a[left], a[mid] = a[mid], a[left]
    i, j = left, right
    while i < j:
        while i < j and a[j] >= temp:  # 一直寻找比旗帜值小的元素下标,找到了便停下来
            j -= 1
        a[i] = a[j]                    # 将该值放到旗帜所指值的左边
        while i < j and a[i] <= temp:  # 一直寻找比旗帜值大的元素下标,找到了便停下来
            i += 1
        a[j] = a[i]  # 放到旗帜所指值的右边
    # 当哈利油和牛A相遇时,他们已经把所有小于旗帜所指金钱的值放到了左边,比旗帜所指值大的金额放到了右边

    a[i] = temp  # 将选取的基准值放到正确的位置上,即每次基准值都归位了
    return i        # 当前的i位置把序列划分为两个部分,然后继续进行递归调用


def quick_sort(a, left, right):
    if (left < right):
        i = partiion(a, left, right)
        quick_sort(a, left, i - 1)  # 处理左半部分
        quick_sort(a, i + 1, right)  # 处理右半部分


if __name__ == '__main__':
    a = [3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48]
    b = [6, 3, 44, 3, 19, 28, 1]
    print(b)
    print(a)
    quick_sort(a, 0, len(a) - 1)
    quick_sort(b, 0, len(b) - 1)
    print(b)
    print(a)
算法分析

空间复杂度:因为快速排序是使用的递归,所以它的空间复杂度为递归栈的深度,在最坏的情况下,它要经过n-1次调用,空间复杂度为 O ( n ) O(n) O(n),但是在平均情况下它的递归调用为 O ( l o g n ) O(logn) O(logn)次,即平均情况下空间复杂度为 O ( l o g n ) O(logn) O(logn)

时间复杂度:快排的运行时间与区间的划分是否对称有着直接联系,在最坏情况下,区间划分为n-1个元素和0个元素,即对应的初始序列,在旗帜的两边分别有序时,此时时间复杂度为 O ( n 2 ) O(n^2) O(n2)。但在很多情况下partiion函数能实现平均划分,在平均情况下快排的时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn)。快速排序是所有内部排序中算法平均性能最优的排序算法。

稳定性:快排在排序过程中,两个相同元素的值会因交换而发生相对位置的变化,所以快排是一种不稳定的排序算法。

碎碎念:

总算搞完了,每天挤一点时间写,QAQ,画图好难画。
希望能帮到正在学习冒泡排序和快排的你。
在这里插入图片描述

  • 38
    点赞
  • 50
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值