本文目录

  • 1、导言
  • 2、谈谈排序
    • 2.1 何为排序?(What is sorting?)
    • 2.2 排序的应用(Why sorting?)
    • 2.3 常见排序算法的种类(How to sort?)
  • 3、基本排序算法详解
    • 3.1 冒泡排序
      • 冒泡排序:概述
      • 冒泡排序:直观演示
      • 冒泡排序:循环临界值的分析与设定
      • 冒泡排序:伪码
      • 冒泡排序:Java实现
      • 冒泡排序:C语言实现
    • 3.2 选择排序
      • 选择排序:概述
      • 选择排序:直观演示
      • 选择排序:循环临界值的分析与设定
      • 选择排序:伪码
      • 选择排序:Java实现
      • 选择排序:C语言实现
    • 3.3 插入排序
      • 插入排序:概述
      • 插入排序:直观演示
      • 插入排序:循环临界值的分析与设定
      • 插入排序:伪码
      • 插入排序:Java实现
      • 插入排序:C语言实现
  • 4、有关复杂度问题
  • 5、心得

1、导言

可能有很多初学者对于“算法”(英:Algorithm,日:アルゴリズム)这个概念还是云里雾里、一知半解、似懂非懂的。我对你的建议是,从排序学起。用多种不同的方法去实现同一件事情,从中去感受它们之间的思路差异性、效率差异性(从计算时间所占用的内存空间两个方面去考究),加深自己对于算法这个概念本身的理解。在这篇文章里,我将为你介绍三种ABC式的入门级基础排序算法。

2、谈谈排序

2.1 何为排序?(What is sorting?)

排序(英:sort,日:整列(せいれつ)/ ソート),就是将一组数值无序的元素(初学阶段就是指数组)排列整齐,使其具备一定的顺序(或是从小到大的正序,或是从大到小的倒序)的过程。

比如有一组无序数值{2,0,1,6,0,6,1,9}吧,这是它的初始状态。将其排序完之后(无论使用哪种排序算法),其目标状态应该是:
{0,0,1,1,2,6,6,9}(正序)
或者是,
{9,6,6,2,1,1,0,0}(倒序)

2.2 排序的应用(Why sorting?)

排序有很多、很广泛的应用。其中,最为典型的案例之一,就是为二分查找(binary search)做铺垫。俗话说的“按顺序一个一个找”,在算法范畴里有个专属名词叫线性查找(linear search)。线性查找实现起来比较简单(对于算法的设计者来说),但是非常地耗时间,其时间复杂度为O(n)。相比之下,二分查找所需要的时间成本,则呈指数级下降,时间复杂度为O(log n)。初学者朋友可能看不懂时间复杂度,这不要紧,这不是本篇文章的重点。为了让大家懂我在说什么,我举个直观点的例子吧。

比如说,你要在104,8576个数据中找到其中的某一个数据。

先来看线性查找。最坏情况,比如说,我设定程序从前往后搜索,而我要找的数据正好在最后一个吧。遇到这种情况,我们的计算机就需要搜索104万次才能找到指定数据。

而如果使用二分查找,104万个数据,计算机却只需要搜索最多20次就可以找到它了。

这得节省多少时间成本啊!数据越多,查找操作的次数越多,二分查找相较于线性查找的优势,就越明显。
而,二分查找对于对象数组的有序性是有要求的。只有在一个有序的数组里,计算机才知道目标数值与当前数值的大小关系,也就是才知道接下来应该往左找还是往右找。
这就是我们学习如何排序一个很重要的原因。也许有人会问,我百万量级的数据排个序还是得计算百万次啊?但你别忘了,在实际开发的过程中,查找操作极有可能被重复使用很多次。一次花100万趟搜索,你重复使用100次就是1亿趟搜索(最坏情况)。而如果你将数组排好序,你最大的花费也就只是排序的100万次罢了,后面的查找你最多每趟20次计算,100趟顶多2000次计算就能搞定,节能效果还是非常显著的。

2.3 常见排序算法的种类(How to sort?)

排序算法有很多。其中,比较经典与常见的有:冒泡排序(bubble sort,バブルソート)、选择排序(selection sort,選択法(せんたくほう))、插入排序(insertion sort,挿入法(そうにゅうほう))、希尔排序(Shell’s sort,シェルソート)、桶排序(bucket sort,バケットソート)、堆排序(heap sort,ヒープソート)、快速排序(quick sort,クイックソート)、归并排序(merge sort,マージソート)……等等等等。

本文主要介绍前三种,即:冒泡排序、选择排序与插入排序。

3、基本排序算法详解

3.1 冒泡排序

冒泡排序:概述

冒泡排序,顾名思义就是使数据按照其值的大小依次像冒泡一样往上冒的算法。
假设我们现在需要让数组按照从小到大的顺序(正序)排列。那么,最大的数,应该在第一次外层大循环中先冒到数组的最尾部,其次大的数应该在第二次大循环中冒到紧贴其前的位置。以此类推,直至所有的数排列整齐为止。而,每一次外层大循环实现其目标的具体做法就是,通过内层小循环按顺序将数值进行两两比较,使每次相比的两数中,比较大的那个数排到后面,比较小的排到前面。以此让数字按照顺序找到它应处的位置。

“冒泡”的印象图(作者:ひとり),图片仅供参考

冒泡排序:直观演示

冒泡排序:循环临界值的分析与设定

很多人会觉得循环逻辑难,其实一个主要的原因就是临界值不好找——要么多出一次循环,要么少一次循环,导致要么运行时报错,要么不是自己想要的结果。临界值不光在编程的范畴,就是在其它学科范畴里,它也是一个永恒的难题。所以,小狐就来手把手带着大家寻找排序算法逻辑里的临界值。
怎么找冒泡排序的循环临界值呢?
先来回顾一下冒泡排序的原理:外层第1次大循环,将第1大的数送到倒数第1个位置;外层第2次大循环,将第2大的数送到倒数第2的位置;外层第3次大循环,将第3大的数送到倒数第3的位置……以此类推,我们首先是不是可以精确地归纳出它所需要的外层大循环总计有多少次?对了,也就是,它总计有多少个数,就进行多少次外层大循环。设数组总计有size个数,那就需要进行size次外层大循环,一次不多,一次不少。
换句话说,
如果从1开始循环,就是从1一直循环到size为止(包含首尾值)
如果从0开始循环,就是从0一直循环到size-1为止(包含首尾值)
这两种写法除了序号不同外,循环的次数是完全相同的,都是size次。
好,分析完了外层循环,接下来看每一次外层循环里的内层循环都做些什么事情。我们知道,在每一次外层循环里,需要对数字从头到尾进行两两比较。也就是,
第1个数和第2个数比,交换或不交换;
第2个数和第3个数比,交换或不交换;
第3个数和第4个数比,交换或不交换;
……
第n个数和第n+1个数比,交换或不交换。
是吧?其实说到这里,内层循环的次数就已经出来个大概了。每次肯定是从第1个数开始比嘛!比到第多少个数为止呢?每一次都要比到末尾(即第size个数)吗?不是的。这样做虽然不会出错,但是会狠浪费资源。冒泡排序本来就是一种引导式、启蒙式的幼儿园级算法,本来就是很低效的,你再去多这些不必要的冗余计算,岂不是低效中的低效?
那么要比对到多少为止,才相对较合适呢?我们发现,i(外层循环)每完成一次循环,就有一个数字被排到它应处的位置去。也就是说,当前外层的i的数值是多少,就表示已经有多少个数被排好了,不需要再理它了。所以,我们首先想到的内层循环临界值,一定是size-i。
这是不错的。
但是我还想请同志们注意一点,那就是j的每次两两对比,是第j个数与第j+1个数进行对比。也就是说,我们想让j的最后一次对比,是第size-i-1个数与第size-i个数的对比。因此,j的临界值最多遍历到数组的第size-i-1个位置就好了。这样的话,最后一次遍历,就是比较第size-i-1个值的大小与第size-i个值的大小,决定是否交换。不会多余计算一个数值,也不会漏计算一个数值。这就是内层循环的临界值。

好了,绕来绕去绕了这么多,总结一下:
设目标数组的长度为size,
则冒泡排序的外层循环i,总计要迭代size次,
内层循环j,总计要迭代size-i-1次。

冒泡排序:伪码

冒泡排序算法的伪码模型如下所示。
另外,关于伪码里的循环控制变量是否包含临界值这个问题,但凡熟悉笔者写伪码风格的同志们都知道——除非我强调不包含,否则一律默认为包含临界值。
而关于两值交换,在我的另一篇文章《【基础算法】编程初学者入门必须掌握的算法——两值交换》里有详解。不懂的同学可以点进去看看。

Algorithm 冒泡排序(数组长度)
Beginfor i = 1 to 数组长度 step 1for j = 1 to (数组长度-i-1) step 1if(第j个元素 > 第j+1个元素) then   //若要改为倒序,将此处判断条件的大于号改作小于号即可交换(第j个元素, 第j+1个元素)end ifnext jnext i
End

冒泡排序:Java实现

import java.util.Arrays;public class BubbleSort{public static void main(String[] args){int[] array = {2,0,1,6,0,6,1,9};System.out.println("排序前:"+Arrays.toString(array));System.out.println("正在进行冒泡排序……");//*******************核心部分*******************for(int i = 0; i < array.length; i++){for(int j = 0, temp; j < array.length - i - 1; j++){if(array[j] > array[j+1]){temp = array[j];array[j] = array[j+1];array[j+1] = temp;}}}//*******************核心部分*******************System.out.println("排序后:"+Arrays.toString(array));}
}

冒泡排序:C语言实现

#include <stdio.h>
#define SIZE 8int main(void){int array[SIZE] = {2,0,1,6,0,6,1,9};printf("排序前:");for(int i = 0; i < SIZE; i++){printf("%d\t", array[i]);}printf("\n正在进行冒泡排序……\n");//*******************核心部分*******************for(int i = 0; i < SIZE; i++){for(int j = 0, temp; j < SIZE-i-1; j++){if(array[j] > array[j+1]){temp = array[j];array[j] = array[j+1];array[j+1] = temp;}}}//*******************核心部分*******************printf("排序后:");for(int i = 0; i < SIZE; i++){printf("%d\t", array[i]);}printf("\n");return 0;
}

3.2 选择排序

选择排序:概述

选择排序就是,从数组尚未排序的一部分范围里找出这个范围的极值(正序就是选最小值,倒序就是选最大值),然后将这个极值排列到已排序的部分的队尾的算法。也就是,在第1次循环中,第1小的值被换到已排序的范围的队尾,在第2次循环中,未排序部分范围里最小的值,即,整体而言的第2小的值,被换到已排序的范围的队尾……以此类推。

选择排序:直观演示

选择排序:循环临界值的分析与设定

还是设数组长度为size。
首先,因为每一次外层循环(即i每迭代一次)的结果都是会产生一个极值出来,使已排序的队列新增一个成员,所以外层循环的总次数还是size次。
i目前的值为多少,就说明前多少个元素已经排好序了。
所以,我们的内层循环是要从已排好序的部分的最末尾,的下一个元素,也就是未排序部分的第一个元素开始的。用代码语言表述,就是int j = i+1。而一直要遍历找到多少为止呢?每一次都是要遍历到数组的最后一个元素为止的。

总结:
设目标数组长度为size,
则选择排序的外层循环i,总计要迭代size次,
内层循环j,要从i+1开始迭代到数组最后一个元素

选择排序:伪码

选择排序算法的伪码模型如下所示(正序)。倒序的情况就不说明了,因为如果你理解了这段代码,改成倒序对你来说很容易。

Algorithm 选择排序(数组长度)
Beginfor i = 1 to 数组长度 step 1min ← 第i个元素minIndex ← ifor j = (i+1) to 数组长度 step 1if(第j个元素 < min) thenmin ← 第j个元素minIndex ← jend ifnext jif(i ≠ minIndex) then交换(第i个元素, 第minIndex个元素)end ifnext i
End

选择排序:Java实现

小提示:将上面的冒泡排序核心部分代码替换即可。

import java.util.Arrays;public class SelectionSort{public static void main(String[] args){int[] array = {2,0,1,6,0,6,1,9};System.out.println("排序前:"+Arrays.toString(array));System.out.println("正在进行选择排序……");//*******************核心部分*******************for(int i = 0, min=-1, minIx=-1, temp; i < array.length; i++){min = array[i];minIx = i;for(int j = i+1; j < array.length; j++){if(array[j] < min){min = array[j];minIx = j;}}if(i != minIx){temp = array[i];array[i] = array[minIx];array[minIx] = temp;}}//*******************核心部分*******************System.out.println("排序后:"+Arrays.toString(array));}
}

选择排序:C语言实现

#include <stdio.h>
#define SIZE 8int main(void){int array[SIZE] = {2,0,1,6,0,6,1,9};printf("排序前:");for(int i = 0; i < SIZE; i++){printf("%d\t", array[i]);}printf("\n正在进行选择排序……\n");//*******************核心部分*******************for(int i = 0, min, minIx, temp; i < SIZE; i++){min = array[i];minIx = i;for(int j = i+1; j < SIZE; j++){if(array[j] < min){min = array[j];minIx = j;}}if(i != minIx){temp = array[i];array[i] = array[minIx];array[minIx] = temp;}}//*******************核心部分*******************printf("排序后:");for(int i = 0; i < SIZE; i++){printf("%d\t", array[i]);}printf("\n");return 0;
}

3.3 插入排序

插入排序:概述

插入排序,就是不断将数组未排序部分的先端元素取出,插入在已排序部分的对应位置的算法。
这种算法应该是今天所介绍的三种算法里,最高效的一种。不信你看我对它的概述都比前面两个少了不少句话呢!
说到它的高效性,就得插一个题外话了。有时候,算法不一定是为编程服务的,偶尔也能解决我们工作生活中的问题。就比如我本科的时候吧,在学校图书馆做兼职。我们有一个业务,也是我们最主要的业务,叫排架。就是将书按照索书号(英:call number,日:請求記号(せいきゅうきごう))放回原位。因为书在书架上是有顺序的,所以如果不加任何预处理,使推车上书籍的索书号处于一个无序状态,推着小车东奔西走的,就会很浪费时间。所以,在排架开始之前,我都会对书本按照各自的索书号进行一个排序操作。
而,我最常用的“算法”,就是这一节所介绍的插入排序法了。在无序的一堆书本里,看第2本书是否应该插在第1本书前面,然后看第3本书应该插在前3本的什么位置,第4本书应该插在前4本的什么位置……以此类推,很快,排序这项预处理就完成了。能想到这样的方法来提升工作效率,也许也得益于自己是个学计算机专业的吧!

插入排序:直观演示

插入排序:循环临界值的分析与设定

还是与刚刚两个一样,一次外层循环,即i每迭代一次,就可以使一个元素被安插到它应处的位置。只不过,与之前不同的是,我们不需要考虑第一个元素了。因为啊,如果集合中只有1个元素,它就可以视作是有序的。所以第一个元素,我们什么都不用做,它就已经归位了。我们直接从第2个元素开始归位就好了。所以,外层循环控制变量的初始值,不再是数组中第1个元素的下标,而是第2个元素的下标。直至最后一个元素的下标为止。
而内层循环呢?也非常简单。我们因为是要在前i个元素中找到它的位置,所以遍历第1个元素到第i个元素即可。找到已排序范围内第一个比它大的数,它的位置就在那个数的前面了。找到位置并处理后,别忘了将内层循环break掉喔!

总结:
设目标数组长度为size,
则插入排序的外层循环i,总计要迭代size次,
内层循环j,要从数组第1个元素起迭代到数组第i个元素为止。中途找到位置了即break。

插入排序:伪码

插入排序算法的伪码模型如下所示:

Algorithm 插入排序(数组长度)
Beginfor i = 2 to 数组长度 step 1for j = 1 to i step 1if(第j个元素 > 第i个元素) then将第i个元素取出,第j个位置起所有元素后挪,然后将第i个元素放置在第j个位置上。breakend ifnext jnext i
End

插入排序:Java实现

import java.util.Arrays;public class InsertionSort{public static void main(String[] args){int[] array = {2,0,1,6,0,6,1,9};System.out.println("排序前:"+Arrays.toString(array));System.out.println("正在进行插入排序……");//*******************核心部分*******************for(int i = 1, temp; i < array.length; i++){for(int j = 0; j < i; j++){if(array[j] > array[i]){temp = array[i];for(int k = i-1; k >= j; k--){array[k+1] = array[k];}array[j] = temp;break;}}}//*******************核心部分*******************System.out.println("排序后:"+Arrays.toString(array));}
}

插入排序:C语言实现

#include <stdio.h>
#define SIZE 8int main(void){int array[SIZE] = {2,0,1,6,0,6,1,9};printf("排序前:");for(int i = 0; i < SIZE; i++){printf("%d\t", array[i]);}printf("\n正在进行插入排序……\n");//*******************核心部分*******************for(int i = 1, temp; i < SIZE; i++){for(int j = 0; j < i; j++){if(array[j] > array[i]){temp = array[i];for(int k = i-1; k>=j; k--){array[k+1] = array[k];}array[j] = temp;break;}}}//*******************核心部分*******************printf("排序后:");for(int i = 0; i < SIZE; i++){printf("%d\t", array[i]);}printf("\n");return 0;
}

4、有关复杂度问题

坦白说,有关算法复杂度的问题,我自己也是一知半解,说不明白、拿不出手的。所以在这里就不进行有关复杂度的详细分析了。如果同志们想要了解更多与排序算法的复杂度有关的内容,不妨看看LYFlied写的《十大排序算法JS实现以及复杂度分析》这篇文章,我认为归纳得很全面,很有参考价值。

5、心得

算法是程序的灵魂。如果你想获得更高的薪资,或者你想用自己所学的计算机专业技术为咱们中国做点突出的贡献,就不能仅仅满足于懂得如何使用API。而是更要会设计算法、优化算法,不断减少程序执行过程中的时间损耗与内存空间损耗。可能在数据量少的情况下,并不能看出来什么差异,但数据的量级一旦庞大起来,以亿万而计的时候,一个更好的算法甚至也足以省下以亿万而计的经济成本。
怎样学好算法呢?怎样提高自己设计算法的能力呢?我暂时还没有一个拿得准的答案,毕竟我自己目前就还只是一届小白,在能力提升这方面没什么发言权。不过,倒是可以根据别人给我的建议,谈一谈我自己的能力提升计划:①重拾中学数学与高等数学,学数学常态化,入木三分,不耻下问;②研读《算法导论》(ISBN:978-7-111-40701-0)常态化,如切如磋,如琢如磨;③做题常态化,一定要多练,实践是检验真理的唯一标准,我个人目前正在使用的是北京大学的在线判定题库POJ系统(http://poj.org/problemlist)。
另外,像分析排序算法的循环控制变量临界值这样的时候,不妨多动手在草稿纸上画画图,或者用excel来模拟内存的数据储存状态。这样或许比凭空想象要更轻松,更能增加我们的信心。适当借助外部工具,为我们的大脑减减负,给它腾出更多的“内存空间”来处理别的事情——这也是算法。
总之,希望大家都能重视起算法,重视起理论设计吧。以知识为马力,奔向梦的远方。

原创不易,给点鼓励!

【基础算法】算法,从排序学起(一)相关推荐

  1. 算法基础:常用的排序算法知识笔记

    1.算法外排序分类 2.冒泡排序 冒泡排序(Bubble Sort)属于交换排序,它的原理是:循环两两比较相邻的记录,如果反序则交换,直到没有反序的记录为止. 实现算法: /** * 冒泡排序优化后的 ...

  2. 1177: 按要求排序(指针专题)_L2算法基础第10课 排序中

    L2-算法基础-第10课 排序中 排序 归并排序 归并排序(Merge sort)是建立在归并操作上的一种有效的排序算法.该算法是采用分治法(Divide and Conquer)的一个非常典型的应用 ...

  3. C/C++程序基础 (九)排序算法简述

    排序算法 算法复杂度 算法简述 插入排序 N2 前方有序,依次将后方无序数据插入前方合适位置. 冒泡排序 N2 前方有序,从后方两两比较,将最小泡冒到前方. 选择排序 N2 前方有序,从后方选择最小的 ...

  4. sqlserver 根据数组排序_看动画学算法之:排序-count排序

    简介 今天我们介绍一种不需要作比较就能排序的算法:count排序. count排序是一种空间换时间的算法,我们借助一个外部的count数组来统计各个元素出现的次数,从而最终完成排序. count排序的 ...

  5. 希尔排序基础java代码_java 算法之希尔排序详解及实现代码

    摘要:这篇Java开发技术栏目下的"java 算法之希尔排序详解及实现代码",介绍的技术点是"希尔排序详解.实现代码.希尔排序.Java.实现.代码",希望对大 ...

  6. Qz学算法-数据结构篇(排序)

    排序算法 排序的概念 排序也称排序算法(Sort Algorithm),排序是将一组数据,依指定的顺序进行排列的过程 分类 排序的分类: 内部排序: 指将需要处理的所有数据都加载到内部存储器中进行排序 ...

  7. php取名字算法,JavaScript排序算法之希尔排序的2个实例_基础知识

    插入排序在对几乎已经排好序的数据操作时, 效率高, 即可以达到线性排序的效率. 但插入排序一般来说是低效的, 因为插入排序每次只能将数据移动一位. 希尔排序按其设计者希尔(Donald Shell)的 ...

  8. 深圳大学算法实验一——排序算法性能分析

    深圳大学算法实验一 一.实验目的与要求 1. 掌握九种排序算法原理 2. 掌握不同排序算法时间效率的经验分析方法,验证理论分析与经验分析的一致性. 3. 对多种排序算法提出改进方案 4. 综合比较各种 ...

  9. 【数据结构与算法】八大排序

    [数据结构与算法]八大排序 数据结构与算法-八大排序 排序的概念及其应用 排序的概念 排序的应用 常见的排序算法实现 常见的排序算法 插入排序 直接插入排序 希尔排序(缩小增量排序) 希尔排序的时间复 ...

最新文章

  1. React学习笔记4: React脚手架配置代理
  2. 真实,假期无限延长后的研究生们的生活~
  3. SAP MM IV中的Duplicated Invoice Check功能的测试
  4. Docker 的优势
  5. python爬取歌曲评论并进行数据可视化
  6. python 算术运算
  7. Visual Studio 2008带来了什么
  8. 分页条件查询_mongodb多条件分页查询的三种方法
  9. Tips on rendering interiors
  10. 12种JavaScript MVC框架之比较
  11. 有人用语音识别写作吗,如果没有,为什么?
  12. 二维码中间嵌入logo
  13. 【Oracle】rollup函数
  14. realtek 8111E 网卡 修改MAC 地址
  15. gromacs ngmx_gromacs示例
  16. loadIdealTree:loadAllDepsIntoIdealTree: sill install loadIdealTree
  17. python中#!含义
  18. vulntarget-b靶场详细通关记录
  19. 内蒙古中考计算机考试知识点总结,内蒙古包头中考语文备考分析及知识总结.doc...
  20. 海康视频VTM流监控浏览器实时播放调试总结

热门文章

  1. EKL相关(一)、安装环境
  2. linux下彻底杀死ngnix进程方法
  3. pandas数据清洗(缺失值、异常值和重复值处理)
  4. 美多商城之商品(商品列表页)
  5. OpenCV标准霍夫直线检测详解
  6. 轻松学Pytorch – 年龄与性别预测
  7. 深度学习之后会是啥?
  8. 超硬核的 Python 数据可视化教程!
  9. 台3岁女童疑把玩风枪致死案疑点多 警方将调查厘清
  10. Qt之两种初始化QListWidget的方法