zoukankan      html  css  js  c++  java
  • 排序

    目录

    • 一、问题和性质
    • 二、简单排序算法
    • 三、快速排序
    • 四、归并排序
    • 五、其他排序

    一、问题和性质

    1、问题定义

    排序就是整理数据的序列,使其中的元素按照特定的顺序排列的操作。

    2、排序算法

    基于比较的排序

    在一个排序中,如果待排序的记录全部保存在内存,这种工作就称之为内排序,针对外存数据的排序称之为外排序。有些算法就适合外排序,这类算法也叫外排算法。

    如果数据本身没有自然的序,根据hash函数设计一个,把数据集的元素映射到某个有序集中。

    基本操作、性质和评价

    根据关键码比较的排序,有两种最重要的基本操作:

    • 比较关键码的操作
    • 移动数据记录的操作

    理论研究证明了一个结论:基于关键码比较的排序问题,时间复杂度是O(nlogn)。有几个算法都达到了这个复杂度。

    空间复杂度在考虑内存排序的时候,有需求,常量的开销,意味着排序工作可以在原列表中完成。具有这种性质的算法也被称为原地排序算法。

    此外排序算法还有两个性质:

    • 稳定性。指的是两个关键码如果相等的话,存放序列还是按照以前的序列。举例:4,3,3,2。排序之后只是将下标0和下标3调换了位置。但是下标1跟下标2没有调换位置。保持了以前的序列。就说这个排序具有稳定性。
    • 适应性。如果一个排序算法对接近有序的序列工作的更快,那么就称这种算法具有适应性。例如:1,2,3,4本身就是排好序的。如果这个算法对于这个序列操作更快,那么就有很好的适应性。

    排序算法的分类

    常见的一种分类是:

    • 插入排序
    • 选择排序
    • 交换排序
    • 分配排序
    • 归并排序
    • 外部排序

    记录结构

    # 一种序列表,其中表中元素是下面的这个对象
    class record:
        def __init__(self, key, datum):
            self.key = key
            self.datum = datum
    被排序的对象

    二、简单排序算法

    1、插入排序 

    将无序区的元素,通过从右向左的方向依次一一比较,然后插入合适的位置。

    # 将排序区设置连续表的左侧
    def insert_sort(lst):
        for i in range(1, len(lst)):
            tmp = lst[i]  # 存放无序区中需要比较的第一个元素
            j = i # 记录向左移动的最大下标值。
            while j > 0 and tmp.key < lst[j-1].key:  # j-1不能小于0,也即是下标不能小于0。并且当无序区取出来的值小于有序区的最大值时,做向左的移动操作。
                lst[j] = lst[j-1]
                j -= 1
            lst[j] =  tmp  # 确定那个值的位置之后,将这个值插入进去。
    插入排序1
    # 将排序区设置为连续表的右侧
    def insert_sort(lst):
        for i in range(len(lst)-2, -1, -1):
            tmp = lst[i]
            j = i
            while j < len(lst)-1 and tmp.key > lst[j+1].key:
                lst[j] = lst[j+1]
                j += 1
            lst[j] = tmp
    插入排序2

    算法分析

    空间复杂度分析,这个算法只用了链各个简单的变量。因此复杂度是O(1)。考虑算法的空间复杂度的时候,是看它为这个算法还有没有额外产生新的存储空间。因此不包含原列表的存储空间。

    时间复杂度分析,外层循环n-1次。内层循环跟实际情况有关,如果是列表是有序的,内层的循环条件就一直不成立。这时是最好的情况,算法复杂度是O(n)。但是如果是完全逆序的话,内层每次都再循环一遍,这时是最坏的情况,复杂度是O(n的平方)。对于一般情况,也是需要内层不断循环,因此平均复杂度是O(n的平方)。

    由上面的分析可以看出这个算法顺序要比逆序要少操作很多。因此它具有适应性。而且,如果遇到两个元素一样,就不会再移动元素了。保存了一样元素的原序列,因此它也具有稳定性。(这里如果把程序改为while j > 0 and tmp.key <= lst[j-1].key的话就没有稳定性了,这代表每次遇到相同的元素都要改变其原有的序列)。

    插入排序算法的变形

    在插入排序中需要检索元素的插入位置,而且是在排序的序列里检索,这就提示了,我们可以使用二分法来检索位置。

    但是,虽然检索代价降低了,但是找到位置之后还是要顺序移动元素,腾出空位将元素插入。这一操作仍然需要线性时间。因此这个变形没有意义。

    相关的问题

    插入排序是最重要的简单排序算法。两点原因:

    • 实现简单
    • 具有稳定性的适应性

    因此它常用来作为一些高级排序算法的组成部分。例如shell排序算法。

    2、选择排序

    基本思路是,从无序区中挑选一个最小的元素,不断放到有序区的最右边。

    def select_sort(lst):
        for i in range(len(lst)-1):
            k = i  # 存放最小元素的下标,起始值为
            for j in range(i+1, len(lst)):
                if lst[j].key < lst[k].key:
                    k = j
            if k != i:
                lst[k], lst[i] = lst[i], lst[k]
        return lst
    选择排序

    从上面代码可以看出,原表不论是顺序还是逆序,内层循环都需要循环一遍找到无序区的最小元素。因此,它不具有适应性。

    此外,上面代码也不具有稳定性。看一个例子。[5,2,3,5,2,6]。循环第一遍,找出最小元素2(下标1)。然后将5(下标0)跟2(下标1)调换。此时列表变成
    [2,5,3,5,2,6]。无序区从下标1开始。找到最小的元素2(下标4)。然后跟5(下标1)调换。这个时候列表变成[2,2,3,5,5,6]。此后i都等于k不需要再调换元素了。序列完成排序。这时候比较一下5元素的序列,原序列处于下标0的5元素是在下标3的5元素前面,但是排序之后,跑到了它的后面。因此,该算法不具有稳定性。

    # 具有稳定性的选择排序算法
    # 思路,取出最小的元素之后,使用整体后移的策略,进行调整元素。而不再仅仅是调换取出的最小元素和无序区第一个元素的位置。
    def select_sort(lst):
        for i in range(len(lst)-1):
            k = i  # 存放最小元素的下标,起始值为
            for j in range(i+1, len(lst)):
                if lst[j].key < lst[k].key:
                    k = j
            if k != i:   
                tmp = lst[k]  #这里移动元素的方法跟插入排序一样。
                while k > i:
                    lst[k] =lst[k-1]
                    k -= 1
                lst[k]=tmp
        return lst
    具有稳定性的选择排序

    算法的时间复杂度在任何情况下都是O(n的平方)。空间复杂度是O(1)。
    从上面代码也可以看出,选择排序的实际平均效率要低于插入排序,并且在各方面都不如插入排序,因此在实际中很少被使用。

    提高选择的效率

    选择排序之所以低效,是因为每次选择元素,都是从头开始做一遍完全比较,实际上可以改进选择方式。那么就是利用树形结构,也就是以前第6章的堆排序算法。

    堆排序见第六章

    3、交换排序

    交换排序的思路:一个序列中没有排好序,那么一定是逆序存在,通过不断减少排序中的逆序,采用不同的确定逆序方法和交换方法,可以得出不同的交换排序方法。气泡排序(也称冒泡排序)就是一种典型的通过交换元素消除逆序实现的排序方法。

    气泡排序

    基本思路:从左到右,比较相邻的两个元素,如果发现逆序,就将两个元素调换,这样。每次遍历完成之后,都有一个最大的元素排序完成。整个过程直至排序结束。

    # 不具有适应性
    def bubble_sort(lst):
        for i in range(len(lst)):
            for j in range(1, len(lst)-i):
                if lst[j].key < lst[j-1].key:
                    lst[j], lst[j-1] = lst[j-1], lst[j]
        return lst
    冒泡排序

    分析上面的代码。可以得出,即使原序列没有逆序,也会一遍遍循环,进行判断比较。所以没有适应性。

    但是具有稳定性,如果相邻的两个元素相等,就不会进行调换,因此它们的原序列不会乱。

    # 具有适应性
    def bubble_sort(lst):
        for i in range(len(lst)):
            found = False  # 存储序列中是否还有逆序的情况。
            for j in range(1, len(lst)-i):
                if lst[j].key < lst[j-1].key:
                    lst[j], lst[j-1] = lst[j-1], lst[j]
                    found = True
            if not found:
                break
        return lst
    具有适应性的冒泡排序

    改良之后,如果原序列中没有逆序,只会在第一遍进行判断,确定没有逆序,证明序列排列完毕,退出循环,直接返回。这样就使气泡排序具有了适应性。

    气泡排序最坏的时间复杂度为O(n的平方),平均复杂度也是O(n的平方),改进的方法在最好的情况下时间开销为O(n)。空间复杂度是O(1)。

    情况分析

    气泡排序的效率要比插入排序的效率低。

    • 反复交换中,赋值操作比较多。
    • 举例最终位置很远的记录,拖累了整个算法。

    因此,针对第二种问题,有一个改良版的气泡排序,就是想办法让元素大步向最终距离移动。

    一个方法是交错起泡,具体做法是,从左向右扫描,下一遍从右向左扫描,交替进行。

    def bubble_sort(lst):
        for i in range(len(lst)):
            found = False  # 存储序列中是否还有逆序的情况。
            for j in range(1, len(lst)-i):
                if lst[j].key < lst[j-1].key:
                    lst[j], lst[j-1] = lst[j-1], lst[j]
                    found = True
            for m in range(len(lst)-i-1, 0, -1):
                if lst[m].key < lst[m-1].key:
                    lst[m], lst[m-1] = lst[m-1], lst[m]
                    found = True
            if not found:
                break
        return lst
    交错冒泡

    三、快速排序

    在各种基于关键码比较的内排序算法中,快速排序是实践中平均速度最快的算法之一。

    快速排序也采用了发现逆序和交换记录位置的方法。在算法中最基本的思想是划分,即按照某种标准把考虑的记录划分为“小记录”和“大记录”,并通过递归不断划分,最终得到一个排序的序列。

    基本过程:

    • 选择一个标准,把排序序列的记录按标准分为大小两组。大的一组在这个标准的右边,小的在左边。
    • 采用同样的方式,递归划分得到的这两个分组记录,并一直递归划分下去。
    • 上面工作,直到每个记录组中包含一个记录时,整个序列排序完成。

    1、快速排序分析

    问题1:排序过程中使用什么空间作为辅助空间完成排序。

    解决:快速排序使用原表内部的空间,将划分标准的记录放到中间,小的一组元素放到左边,大的一组元素放到右边。

    问题2:怎么确定这个标准。

    解决:快速排序采用最简单的方式,用序列中的./img第一个记录作为标准。

    划分实现

    现在考虑一次划分的实现:

    • 取出第一个记录R作为标准(图a)。
    • 这时有一个空位出来了(图b)。
    • 然后从右向左检查,把遇到的第一个小于R的元素,放到空位。然后右边又有一个空位(图c)。
    • 再从左到右把第一个大于R的元素放到那个空位。
    • 直至图中i和j重合。
    • 最后将R存入剩余的那个空位。
    • 一次划分的工作就完成。

    一次划分完成之后,两边子序列按照同样的方式递归处理。由于要做两个递归,快速排序算法的执行形成了一种二叉树形式的递归调用。

    2、程序实现

    def quick_sort(lst):
        qsort_rec(lst, 0, len(lst)-1)
    
    def qsort_rec(lst, l, r):
        if l >= r:   # 没有分段记录或者只有一个分段记录了。
            return
        i, j = l, r
        pivot = lst[i] # 用来划分的标准元素
        while i < j:
            while i < j and pivot <= lst[j]: #从右向左找到第一个小于pivot的元素下标j。
                j -= 1
            if i < j:   # 说明存在这样的元素,那么就让其填补左边的空位。
                lst[i] = lst[j]
                i += 1
            while i < j and pivot >= lst[i]: #再从左往右找到第一个大于pivot的元素下标i。
                i += 1
            if i < j:   # 说明找到这样的元素,让其填补右边的空位。
                lst[j] = lst[i]
                j -= 1
        lst[i] = pivot  # 将原来的标准元素放到最后那个空位中
        qsort_rec(lst, l, i-1)  # 将划分好的左半部分进行递归调用再次进行划分
        qsort_rec(lst, i+1, r)  # 将划分好的右半部分进行递归调用再次进行划分
    快速排序

    3、复杂度

    时间复杂度

    整个算法中,元素移动的的次数要小于比较的次数,因此只需要考虑比较的次数。

    每次划分都需要把处理区域分为两段。很显然,需要logn层划分。

    而一次划分处理,需要循环整个序列表来确定大小区域。因此需要O(n)。

    综合起来就是O(nlogn)。

    但是,有最坏的情况,比如每次划分完之后总有一段是空的。也就是当序列表完全是顺序或者逆序的情况下,划分区的时候,就有一段是空的。那么这个时候时间复杂度是O(n的平方)。

    由此可以看出,快速排序的性能影响主要是分段的不均衡,根源是划分标准没有取好。可以考取修改划分的依据。例如:三者取中的规则。

    空间复杂度

    空间复杂度与实现方式有关。如果是递归实现,执行方式由解释器确定,不容易控制。可以在程序里引入一个栈保存未处理的分段信息,采用非递归方式实现快速排序算法。这样栈的深度就不会超过O(logn),因此快速排序的辅助空间可以做到O(logn)。

    算法性质

    不稳定的,更不适用(适得其反)。

    4、另外一种简单实现

    这个算法在运行中一次划分的中间状态,如上图所示。
    工作过程中将本分段划分三组:小记录,大记录,未检查的记录。i的值是最后一个小记录的下标,j的值是第一个未处理记录的下标。

    每次迭代比较j和R。

    • 如果j记录大,j+1,恢复到图中的情况。
    • 如果j记录小,这时需要把j记录调整到左边。具体做法是i + 1,而后i和j交换位置,j+1。恢复到图中情况。

    划分完成后,把R放到正确位置。也就是交换它和i的位置。因为i本来就是小记录组的最后一个元素,跟第一R元素交换位置,i元素还是处于小记录组。而R正好处于小记录与大记录的分界线上。

    def quick_sort1(lst):
        def qsort(lst, begin, end):
            if begin >= end:
                return
            pivot = lst[begin].key
            i = begin
            for j in range(begin + 1, end + 1):
                if lst[j].key < pivot:
                    i += 1
                    lst[i], lst[j] = lst[j], lst[i]
            lst[begin], lst[i] = lst[i], lst[begin]
            qsort(lst, begin, i-1)
            qsort(lst, i + 1, end)
        
        qsort(lst, 0, len(lst) - 1)
    快速排序的另一种实现

    四、归并排序

    归并排序也是一种典型的排序,它的基本思想是把两个或者更多有序序列合并为一个有序序列。

    • 初始时,先把待排序序列中的n个记录看成n个有序子序列。
    • 再把有序子序列两两归并,完成一遍之后序列组内的排序序列个数减半,每个子序列的长度加倍。
    • 对加长的有序子序列重复上面操作,最终得到一个长度为n的有序序列。

    这种方法是二路归并排序。每次操作都是把两个有序序列合并为一个有序序列。当然,也可以考虑三路归并或者更多路的归并。

    由此可见,归并操作是一种顺序操作,因此比较适合外存数据,因为外村数据也是顺序处理。

    1、顺序表的归并排序

    下面讨论顺序表的二路归并排序算法。先看一个例子,了解一下归并算法的思想。

    2、归并算法的设计问题

    问题:把归并结果的序列放到哪里

    解决:一种简便的处理方式是为归并另外开辟一片同样大小的存储区。然后把一遍的归并放到那里,再进行归并的时候,可以把新生成的表放到原表中,就这样两个表交替放置。最终完成整个排序工作。

    采用这种方式,至少需要O(n)的辅助空间,这是明显的付出空间代价,来获得时间性能。

    当然,也有原地排序的算法,只是都比较复杂。

    3、归并排序函数定义

    归并算法的实现可以分为三层:

    • 最下层:实现表中相邻的一对有序序列的归并工作,并将归并的结果存入到另外一个顺序表里的相同位置。
    • 中间层:基于操作1(一对序列的归并操作),实现将整个表里顺序各对有序序列的归并,完成一遍归并,然后将归并结果存入到另一个顺序表的相同表里的分段位置。
    • 最高层在两个顺序表之间往复执行操作2,完成一遍归并后交换两个表的地位,然后再重复操作2的工作,直至整个表里只有一个有序序列时完成排序。
    # 最下层合并一对有序序列的函数
    def merge(lfrom, lto, low, mid, high):
        '''
        lfrom:被归并的有序段来源的表中
        lto:被归并的有序段存放的表中
        lfrom[low:mid]:需要归并的第一个有序段
        lfrom[mid:high]:需要归并的第二个有序段
        lto[low:high]:归并完成之后的有序段
        '''
        i, j, k = low, mid, low
        while i < mid and j < high: # 反复复制两个分段首记录中比较小的元素
            if lfrom[i].key < lfrom[j].key: #前半段元素小
                lto[k] = lfrom[i]
                i += 1
            else: # 后半段中的元素小
                lto[k] = lfrom[j]
                j += 1
            k += 1
        while i < mid: #后半段完成合并,前半段还有元素
            lto[k] = lfrom[i]
            i, k = i+1, k+1
        while j < mid: #前半段完成合并,后半段还有元素
            lto[k] = lfrom[j]
            j, k = j+1, k+1
    
    # 中间层,对所有的相邻两段进行合并,它需要知道表长度和分段长度
    def merge_pass(lfrom, lto, llen, slen):
        '''
        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:  # 剩下最后两段,并且后段的长度小于slen
            merge(lfrom, lto, i, i + slen, llen)
        else:  # 只剩下一段,复制到表lto
            for j in range(i, llen):
                lto[j] = lfrom[j]
    
    # 最后是主函数。它安排了另外的一个同样长度的表,在两个表之间往复做一遍遍的归并,知道完成工作。
    
    def merge_sort(lst):
        slen, llen = 1, len(lst)
        templst = [None] * llen
        while slen < llen:
            merge_pass(lst, templst, llen, slen)
            slen *= 2
            merge_pass(templst, lst, llen, slen)
            slen *= 2    
    归并排序

    4、算法分析

    时间复杂度O(nlogn)
    空间复杂度O(n)
    没有适应性,没有稳定性。

    五、其他排序方法

    1、分配排序和基数排序

    前面的几种排序算法都是在基于关键码的比较的排序算法。而下面的几种算法都是基于一种固定位置的分配和收集,这是两种不同的思想。

    分配和排序

    如果关键码只有很多几个不同的值,但是有很多个对象。那么下面是一种比较好的算法

    • 为每个关键码设定一个桶(能够容纳任意多个记录的容器,可以是一个连续表)。
    • 排序时简单的根据关键码,把记录放到相应的桶中。
    • 存入所有记录之后,顺序手机各个桶里的记录,就得到了排序的序列。


    这种排序算法通常应用在值很少,但是对象很多的情况下。例如,对全校同学的学习记录排序。由于绩点只有两位数,通常总共有几个可能值。那么为每个绩点设置一个桶。通过一遍分配,一遍收集,就得到了一个稳定排序的结果。如果实现合理,这个算法在O(n)的时间内就完成了,复杂度低于采用关键码比较排序的算法。比如:给[1,2,2,1,2,1,2,1,2,1,2,2,2,1]这样的序列排序。

    但如果是关键码的取值集合非常大,通常不适用,因为要建立大量的同,而且这些桶在实际中通常是空的。比如:给[1,999999999999,34,263,64]这样的序列进行排序。

    2、python系统的list排序

    sort,可以对任何可迭代对象排序,得到一个排序表。list也有一个sort的方法,他们共享一个排序算法Timsort,蒂姆排序。

    基本情况
    蒂姆排序是一种基于归并排序的稳定排序算法,其中还结合了插入排序算法,该算法具有适用性。其最坏时间复杂度是O(nlogn)。最坏情况下,需要的空间复杂度是O(n)。蒂姆排序是目前实际表现最好的排序算法。

    它的主要优势是克服了归并排序没有适用性的缺陷,又保证了其稳定性,并尽可能利用实际数据的情况。
    基本过程:

    • 考察待排序序列中严格单调上传或者严格单调下降的片段,反转其中严格下降的片段。
    • 采用插入排序,对连续出现的几个特别短的上升序列排序,使整个序列变成一系列单调上升的片段。
    • 通过归并产生更长的排序片段,控制这一归并过程,保证片段的长度尽可能均匀。然后反复归并最终得到排序序列。

    3、总结

  • 相关阅读:
    Cocos2dx-lua开发之c++绑定到lua
    SCOPE_IDENTITY和@@IDENTITY[转]
    c#List结合IEqualityComparer求交集
    K:java中正则表达式的使用说明及其举例
    K:常见的正则表达式
    K:正则表达式之基础简介
    Q:javax.comm 2.0 windows下Eclipse的配置
    Q:同时安装了python2和python3的电脑下pip的使用
    K:java 断言 assert 初步使用:断言开启、断言使用
    请描述一下 cookies,sessionStorage 和 localStorage 的区别
  • 原文地址:https://www.cnblogs.com/walle-zhao/p/11390286.html
Copyright © 2011-2022 走看看