排序算法总结

剑指offer已经刷完一遍了,今天开始学习整理一下排序算法。争取每天整理两个。
参考博客:http://www.cnblogs.com/feixuelove1009/p/6143539.html

2018.10.19

一、排序的概念和分类

1、排序的稳定性:

经过某种排序后,如果两个记录序号同等,且两者在原无序记录中的先后秩序依然保持不变,则称所使用的排序方法是稳定的,反之是不稳定的。
举个例子,要排序的内容是一组原本按照价格高低排序的对象,如今需要按照销量高低排序,使用稳定性算法,可以使得想同销量的对象依旧保持着价格高低的排序展现,只有销量不同的才会重新排序。


image.png

该图有个错的地方,简单选择排序是不稳定的算法。

2、内排序和外排序:

内排序:排序过程中,待排序的所有记录全部放在内存中
外排序:排序过程中,使用到了外部存储。
通常讨论的都是内排序。

3、影响内排序算法性能的三个因素:

时间复杂度:即时间性能,高效率的排序算法应该是具有尽可能少的关键字比较次数和记录的移动次数
空间复杂度:主要是执行算法所需要的辅助空间,越少越好。
算法复杂性:主要是指代码的复杂性。

二、冒泡排序

冒泡排序(Bubble sort):时间复杂度O(n^2)
交换排序的一种。其核心思想是:两两比较相邻记录的关键字,如果反序则交换,直到没有反序记录为止。
先定义一个交换元素位置的函数:

class SQList():

    def __init__(self, nums):
        self.nums = nums

    def swap(self, i, j):
        '''将nums数组的第i个元素和第j个元素交换位置'''
        self.nums[i] , self.nums[j] = self.nums[j], self.nums[i]
        return

冒泡排序是最简单的一种排序方法。
可以细分为以下三种:

1、简单排序

简单排序的思想很简单,将第一个元素与后面所有元素依次进行比较,如果第一个元素更大,则交换两元素的位置,经过一轮比较后,第一个元素就是该列表中最小的数,然后再用第二个元素依次与后面的元素比较。时间复杂度为O(n^2)

def simple_sort(self):
    '''简单排序'''
    for i in range(len(self.nums)):
        for j in range(i+1, len(self.nums)):
            if self.nums[i] > self.nums[j]:
                self.swap(i, j)
    return self.nums

2、冒泡排序

冒泡排序虽然也是两两比较,但是跟简单排序不一样,冒泡排序是每两个相邻的元素比较,如果大小相反则交换,如果从数组第一个元素开始比较的话,经过一轮后,最大的数就排到了数组的最后一位,然后再接着排第二大的数。时间复杂度为O(n^2)

def simple_bubble(self):
    '''简单冒泡排序'''
    j = 0
    while j <= len(self.nums)-1:
        for i in range(len(self.nums)-j-1):
            if self.nums[i] > self.nums[i+1]:
                self.swap(i, i+1)
        j += 1
    return self.nums

3、改进版冒泡排序

改进的冒泡排序的思路跟冒泡排序是一样的,不过增加了一个提前中止的条件,如果在某一轮比较中,没有任何两个元素交换位置,那么此时的序列已经是有序的,可以提前中止。时间复杂度为O(n^2)

def bubble(self):
    '''改进版冒泡排序'''
    j = 0
    while j <= len(self.nums)-1:
        flag = False
        for i in range(len(self.nums)-j-1):
            if self.nums[i] > self.nums[i+1]:
                self.swap(i, i+1)
                flag = True
        if not flag:
            return self.nums
        j += 1
    return self.nums

冒泡排序整理完了,下面开始第二大排序算法

三、简单选择排序

冒泡排序是每次比较如果位置错了就会交换一次位置,换了很多次位置后才确定了一个元素的位置。那么我们可不可以只用一次交换就确定一个元素的位置呢?当我们要排第一个位置的元素时,我们可以找到第一个以后所有元素的最小值,再和第一个位置比较,如果位置错误,那么就交换位置。那样我们虽然遍历了整个列表,但是只用了一次交换函数,虽然时间复杂度跟冒泡排序一样,但是只调用了一次交换函数,所以相对冒泡排序还是有一定程度的改进。时间复杂度为O(n^2)

def select_sort(self):
    '''简单选择排序'''
    for i in range(len(self.nums)-1):
        temp = [None, -1]
        for j in range(i, len(self.nums)):
            if not temp[0]:
                temp = [self.nums[j], j]
            if self.nums[j] < temp[0]:
                temp = [self.nums[j], j]
        if temp[0] < self.nums[i]:
            self.swap(i, temp[1])
    return self.nums

2018.10.20

四、直接插入排序

直接插入排序的思想是将一个新的元素插入到前面排好序的序列中。时间复杂度为O(n^2)

def insert_sort(self):
    '''插入排序'''
    for i in range(1, len(self.nums)):
        j = i - 1
        temp = self.nums[i]
        while j >= 0 and temp < self.nums[j]:
            self.nums[j+1] = self.nums[j]
            j -= 1
        self.nums[j+1] = temp
    return self.nums

抄原文一张图


image.png

五、希尔排序

希尔排序是对直接插入排序的改进版,其核心思想是将原始数据集分割成若干个子序列,然后再对子序列分别进行插入排序,使子序列基本有序,最后再对全体进行一次插入排序。
这里最关键的是跳跃和分割的策略,也就是我们要怎么分割数据,间隔多大的问题。通常将相距某个“增量”的记录组成一个子序列,这样才能保证在子序列内分别进行直接插入排序后得到的结果是基本有序而不是局部有序。下面的例子中通过:increment = int(increment/3)+1来确定“增量”的值。
第一遍没看明白,后来查了其它的资料,才完全明白是怎么回事。以下内容来自博客:https://www.cnblogs.com/chengxiao/p/6104371.html

image.png

image.png

希尔排序的时间复杂度为:O(n^(3/2))
根据这个思路自己写的代码:

def shell_sort(self):
    def insertSortPerGap(nums, gap):
        for i in range(1, len(nums)):
            j = i - gap
            temp = nums[i]
            while j >= 0 and temp < nums[j]:
                nums[j+gap] = nums[j]
                j -= gap
            nums[j + gap] = temp
        return
    gap = int(len(self.nums)/2)
    while gap>=1:
        insertSortPerGap(self.nums, gap)
        gap = int(gap/2)
    return self.nums

2018.10.21

六、堆排序

参考的也是写希尔排序的博客:https://www.cnblogs.com/chengxiao/p/6129630.html
堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序,它的最好、最坏、平均时间复杂度为O(nlogn),它是不稳定排序
堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆,或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。
我们用简单的公式来描述一下堆的定义就是(索引从0开始):
大顶堆:arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2]
小顶堆:arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2]
堆排序的基本思想是:将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根结点。将其与末尾元素进行交换,此时末尾就是最大值。然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值,如此反复执行,就能得到一个有序序列。
一般升序采用大顶堆,降序采用小顶堆
所以堆排序的关键就在于如何将一个序列构造成一个堆。可以从最后一个非叶子结点开始,从左至右,从下至上进行调整。根据完全二叉树的性质,可以知道最后一个非叶子结点的索引就是len(array)//2 - 1(根结点索引为0),那么它之前的所有结点都是非叶子结点,只需要按照这个顺序去构造大顶堆或小顶堆就可以了。
以下自己用python实现的代码:

def heap_sort(self):

    def heap_adjust(nums, length):
        index_root = int(length // 2 - 1)
        while index_root >= 0:
            index_kid1 = index_root * 2 + 1
            index_kid2 = index_root * 2 + 2
            if nums[index_root] < nums[index_kid1]:
                self.swap(index_root, index_kid1)
            if index_kid2 <= length - 1:
                if nums[index_root] < nums[index_kid2]:
                    self.swap(index_root, index_kid2)
            index_root -= 1

    length = len(self.nums)
    while length >= 2:
        heap_adjust(self.nums, length)
        self.swap(0, length - 1)
        length -= 1
    return self.nums

七、归并排序

思想比较简单,参考https://www.cnblogs.com/chengxiao/p/6194356.html
归并排序(MERGE-SORT)是利用归并的思想实现的排序方法,该算法采用经典的分治(divide-and-conquer)策略(分治法将问题分(divide)成一些小的问题然后递归求解,而治(conquer)的阶段则将分的阶段得到的各答案"修补"在一起,即分而治之)。

image.png

这张图大概能看出整个流程了。
归并排序是稳定排序,它也是一种十分高效的排序,能利用完全二叉树特性的排序一般性能都不会太差。从上文的图中可看出,每次合并操作的平均时间复杂度为O(n),而完全二叉树的深度为|log2n|。总的平均时间复杂度为O(nlogn)。而且,归并排序的最好,最坏,平均时间复杂度均为O(nlogn)
很遗憾的是,归并算法我没写出来😔。合并的操作写出来了,但是前面那个递归调用的时候有点乱,没写出来,但其实就是个跟二叉树差不多的操作过程。参考别人的代码再重新码了一遍。

def merge_sort(self):

    def msort(nums):
        if len(nums) <= 1:
            return nums
        mid = len(nums)//2
        left = msort(nums[:mid])
        right = msort(nums[mid:])
        return merge(left+right)

    def merge(numbers):
        temp = [0] * len(numbers)
        i = 0
        j = len(numbers) // 2
        index = 0
        while i < len(numbers) // 2 and j < len(numbers):
            if numbers[i] < numbers[j]:
                temp[index] = numbers[i]
                index += 1
                i += 1
            else:
                temp[index] = numbers[j]
                index += 1
                j += 1
        if i < len(numbers) // 2:
            temp[index:] = numbers[i:len(numbers) // 2]
        else:
            temp[index:] = numbers[j:]
        numbers = temp
        return numbers
    return msort(self.nums)

2018.10.22
今天就只剩最后一个快速排序了。

八、快速排序

快速排序(Quick Sort)由图灵奖获得者Tony Hoare发明,被列为20世纪十大算法之一。冒泡排序的升级版,交换排序的一种。快速排序的时间复杂度为O(nlog(n))

快速排序算法的核心思想:通过一趟排序将待排记录分割成独立的两部分,其中一部分记录的关键字均比另一部分记录的关键字小,然后分别对这两部分继续进行排序,以达到整个记录集合的排序目的。

核心的交换代码我自己写的比较复杂,以下是我自己写的:

def splitByK(numbers):
    times = 0
    leftindex = 0
    rightindex = len(numbers) - 1
    k = numbers[0]
    while rightindex > leftindex:
        if times % 2 == 0:
            if numbers[rightindex] < k:
                numbers[leftindex] = numbers[rightindex]
                times += 1
                leftindex += 1
            else:
                rightindex -= 1
        if times % 2 == 1:
            if numbers[leftindex] >= k:
                numbers[rightindex] = numbers[leftindex]
                times += 1
                rightindex -= 1
            else:
                leftindex += 1
    numbers[rightindex] = k

然后参考别人的代码思路,重新码了一遍:

def partition(nums):
    leftindex = 0
    rightindex = len(nums) - 1
    k = nums[0]
    while leftindex < rightindex:

        while leftindex < rightindex and nums[rightindex] > k:
            rightindex -= 1
        swap(nums, leftindex, rightindex)

        while leftindex < rightindex and nums[leftindex] < k:
            leftindex += 1
        swap(nums, leftindex, rightindex)

最后是完整代码:

def quick_sort(self):

    def partition(nums, leftindex, rightindex):
        leftindex = leftindex
        rightindex = rightindex
        k = nums[leftindex]
        while leftindex < rightindex:

            while leftindex < rightindex and nums[rightindex] > k:
                rightindex -= 1
            self.swap(leftindex, rightindex)

            while leftindex < rightindex and nums[leftindex] < k:
                leftindex += 1
            self.swap(leftindex, rightindex)
        return leftindex

    def qsort(nums, low, high):
        if low < high:
            pivot = partition(nums, low, high)
            qsort(nums, 0, pivot)
            qsort(nums, pivot+1, high)

    qsort(self.nums, 0, len(self.nums)-1)
    return self.nums

快速排序的时间性能取决于递归的深度。
当pivot_key恰好处于记录关键码的中间值时,大小两区的划分比较均衡,接近一个平衡二叉树,此时的时间复杂度为O(nlog(n))。
当原记录集合是一个正序或逆序的情况下,分区的结果就是一棵斜树,其深度为n-1,每一次执行大小分区,都要使用n-i次比较,其最终时间复杂度为O(n^2)。
在一般情况下,通过数学归纳法可证明,快速排序的时间复杂度为O(nlog(n))
但是由于关键字的比较和交换是跳跃式的,因此,快速排序是一种不稳定排序。
同时由于采用的递归技术,该算法需要一定的辅助空间,其空间复杂度为O(logn)

最后再补充一点各个排序的应用场景,参考:https://blog.csdn.net/mbuger/article/details/67643185

(1)若n较小(如n≤50),可采用直接插入或直接选择排序。
 当记录规模较小时,直接插入排序较好;否则因为直接选择移动的记录数少于直接插人,应选直接选择排序为宜。
(2)若文件初始状态基本有序(指正序),则应选用直接插人、冒泡或随机的快速排序为宜;
(3)若n较大,则应采用时间复杂度为O(nlgn)的排序方法:快速排序、堆排序或归并排序。
 快速排序是目前基于比较的内部排序中被认为是最好的方法,当待排序的关键字是随机分布时,快速排序的平均时间最短;
 堆排序所需的辅助空间少于快速排序,并且不会出现快速排序可能出现的最坏情况。这两种排序都是不稳定的。
 若要求排序稳定,则可选用归并排序。但前面介绍的从单个记录起进行两两归并的排序算法并不值得提倡,通常可以将它和直接插入排序结合在一起使用。先利用直接插入排序求得较长的有序子序列,然后再两两归并之。因为直接插入排序是稳定 的,所以改进后的归并排序仍是稳定的。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,530评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,403评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,120评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,770评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,758评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,649评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,021评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,675评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,931评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,751评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,410评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,004评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,969评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,042评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,493评论 2 343

推荐阅读更多精彩内容