排序算法

排序算法可以分为内部排序外部排序

  1. 待排序的记录数不太多:所有的记录都能存放在内存中进行排序,称为内部排序;
  2. 待排序的记录数太多:所有的记录不可能存放在内存中,排序过程中必须在内、外之间进行数据交换。

内部排序的基本操作:

对内部排序而言,其基本操作有两种:

  • 比较两个关键字的大小;
  • 存储位置的移动:从一个位置移动到另一个位置。

第一种操作是必不可少的;而第二种操作却不是必须的,取决于记录的存储方式,具体情况是:

(1)记录存储在一组连续地址的存储空间:记录之间的逻辑顺序关系是通过其物理存储位置的相邻来体现,记录的移动是必不可少的;

(2)记录采用链式存储方式:记录之间的逻辑顺序关系是通过结点中的指针来体现,排序过程仅需修改结点的指针,而不需要移动记录;

(3)记录存储在一组连续地址的存储空间:构造一个辅助表来保存各个记录的存放地址(指针):排序过程不需要移动记录,而仅需修改辅助表中的指针,排序后视具体情况决定是否调整记录的存储位置。

(1)比较适合记录数较少的情况;而(2)和(3)则适合记录数较少的情况。

为讨论方便,假设待排序的记录是以(1)的情况存储,且设排序是按升序排列的;关键字是一些可直接用比较运算符进行比较的类型。

而外部排序是因排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存。

常见的内部排序算法有:插入排序、希尔排序、选择排序、冒泡排序、归并排序、快速排序、堆排序、基数排序等。

关于时间复杂度:

  1. 平方阶(O(n2))排序各类简单排序:直接插入、直接选择和冒泡排序;
  2. 线性对数阶(O(nlog2n))排序:快速排序、堆排序和归并排序;
  3. O(n1+k)排序,k是介于0和1之间的常数:希尔排序;
  4. 线性阶(O(n))排序:基数排序,此外还有桶,箱排序。

关于稳定性:

  1. 稳定的排序算法:冒泡排序、插入排序、归并排序和基数排序;
  2. 不是稳定的排序算法:选择排序、快速排序、希尔排序、堆排序。

冒泡排序:(稳定)

冒泡排序的原理非常简单,它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。

步骤:

  1. 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
  2. 对第0个到第n-1个数据做同样的工作。这时,最大的数就“浮”到了数组最后的位置上。
  3. 针对所有的元素重复以上的步骤,除了最后一个。
  4. 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。

动画演示:
数据结构与算法学习笔记-排序算法
算法代码(基于python):

def bubble_sort(arry):
    n = len(arry)                   #获得数组的长度
    for i in range(n):
        for j in range(1,n-i):
            if  arry[j-1] > arry[j] :       #如果前者比后者大
                arry[j-1],arry[j] = arry[j],arry[j-1]      #则交换两者
    print(arry)
    return arry


arry = [5,4,7,9,2,1,6]
bubble_sort(arry)   


#实验结果:1,2,4,5,6,7,9

选择排序:(不稳定)

  1. 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置
  2. 再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
  3. 重复第二步,直到所有元素均排序完毕。

动画演示:
数据结构与算法学习笔记-排序算法
算法代码(基于python):

def select_sort(ary):
    n = len(ary)
    for i in range(0,n):
        min = i                             #最小元素下标标记
        for j in range(i+1,n):
            if ary[j] < ary[min] :
                min = j                     #找到最小值的下标
        ary[min],ary[i] = ary[i],ary[min]   #交换两者
    print(ary)
    return ary
ary = [6,4,3,1,7,4,9,2]
select_sort(ary)

#实验结果:1,2,3,4,4,6,7,8

插入排序:(稳定)

  1. 从第一个元素开始,该元素可以认为已经被排序
  2. 取出下一个元素,在已经排序的元素序列中从后向前扫描
  3. 如果被扫描的元素(已排序)大于新元素,将该元素后移一位
  4. 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置
  5. 将新元素插入到该位置后
  6. 重复步骤2~5

动画演示:
数据结构与算法学习笔记-排序算法
算法代码(基于python):

def insert_sort(ary):
    n = len(ary)
    for i in range(1,n):
        if ary[i] < ary[i-1]:
            temp = ary[i]
            index = i           #待插入的下标
            for j in range(i-1,-1,-1):  #从i-1 循环到 0 (包括0)
                if ary[j] > temp :
                    ary[j+1] = ary[j]
                    index = j   #记录待插入下标
                else :
                    break
            ary[index] = temp
    print(ary)
    return ary

ary = [1,4,5,0,8,2,7,3,9,0]
insert_sort(ary)

#实验结果:0,0,1,2,3,4,5,7,8,9

希尔排序:(不稳定)

把待排序的数据元素分成若干个小组,对同一小组内的数据元素用直接插入法排序;小组的个数逐次减少;当完成了所有数据元素都在一个组内的排序后,排序过程结束。希尔排序又称作缩小增量排序。

  1. 选择一个增量序列 t1,t2,……,tk,其中 ti > tj, tk = 1;
  2. 按增量序列个数 k,对序列进行 k 趟排序;
  3. 每趟排序,根据对应的增量 ti,将待排序列分割成若干长度为 m 的子序列,分别对各子表进行直接插入排序。仅增量因子为 1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。

动画演示:
数据结构与算法学习笔记-排序算法
算法代码(基于python):

def shell_sort(ary):
    n = len(ary)
    gap = round(n/2)       #初始步长 , 用round四舍五入取整
    while gap > 0 :
        for i in range(gap,n):        #每一列进行插入排序 , 从gap 到 n-1
            temp = ary[i]
            j = i
            while ( j >= gap and ary[j-gap] > temp ):    #插入排序
                ary[j] = ary[j-gap]
                j = j - gap
            ary[j] = temp
        gap = round(gap/2)                     #重新设置步长
    print(ary)
    return ary

ary = [1,5,7,2,9,4,3,1,0]
shell_sort(ary)

#实验结果:0,1,1,2,3,4,5,7,9

归并排序:(稳定)

归并排序主要是二路归并排序。

归并排序是采用分治法的一个非常典型的应用。归并排序的思想就是先递分解数组,再并数组。

先考虑合并两个有序数组,基本思路是比较两个数组的最前面的数,谁小就先取谁,取了后相应的指针就往后移一位。然后再比较,直至一个数组为空,最后把另一个数组的剩余部分复制过来即可。

  1. 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列;
  2. 设定两个指针,最初位置分别为两个已经排序序列的起始位置;
  3. 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置;
  4. 重复步骤 3 直到某一指针达到序列尾;
  5. 将另一序列剩下的所有元素直接复制到合并序列尾。

动画演示:
数据结构与算法学习笔记-排序算法
算法代码(基于python):

def merge_sort(ary):
    if len(ary) <= 1 : return ary
    num = int(len(ary)/2)       #二分分解
    left = merge_sort(ary[:num])
    right = merge_sort(ary[num:])
    return merge(left,right)    #合并数组

def merge(left,right):
    '''合并操作,
    将两个有序数组left[]和right[]合并成一个大的有序数组'''
    l,r = 0,0           #left与right数组的下标指针
    result = []
    while l<len(left) and r<len(right) :
        if left[l] < right[r]:
            result.append(left[l])
            l += 1
        else:
            result.append(right[r])
            r += 1
    result += left[l:]
    result += right[r:]
    print(result)
    return result

ary = [5,6,2,1,9,7,0,1,3]
merge_sort(ary)

#实验结果:0,1,1,2,3,5,6,7,9

快速排序:(不稳定)

快速排序是一种二叉树结构的交换排序方法。

  1. 从数列中挑出一个元素,称为 “基准”(pivot);
  2. 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
  3. 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序;

动画演示:
数据结构与算法学习笔记-排序算法
算法代码(基于python):

def quick_sort(ary):
    return qsort(ary,0,len(ary)-1)

def qsort(ary,left,right):
    #快排函数,ary为待排序数组,left为待排序的左边界,right为右边界
    if left >= right : return ary
    key = ary[left]     #取最左边的为基准数
    lp = left           #左指针
    rp = right          #右指针
    while lp < rp :
        while ary[rp] >= key and lp < rp :
            rp -= 1
        while ary[lp] <= key and lp < rp :
            lp += 1
        ary[lp],ary[rp] = ary[rp],ary[lp]
    ary[left],ary[lp] = ary[lp],ary[left]
    qsort(ary,left,lp-1)
    qsort(ary,rp+1,right)
    print(ary)
    return ary

ary = [5,1,4,8,9,2,0,2,7,3]
quick_sort(ary)

#实验结果:0,1,2,3,4,5,7,8,9

堆排序:(不稳定)

二叉堆具有以下性质:

  1. 父节点的键值总是大于或等于(小于或等于)任何一个子节点的键值。
  2. 每个节点的左右子树都是一个二叉堆(都是最大堆或最小堆)。

步骤:

  1. 构造最大堆(Build_Max_Heap):若数组下标范围为0~n,考虑到单独一个元素是大根堆,则从下标n/2开始的元素均为大根堆。于是只要从n/2-1开始,向前依次构造大根堆,这样就能保证,构造到某个节点时,它的左右子树都已经是大根堆。
  2. 堆排序(HeapSort):由于堆是用数组模拟的。得到一个大根堆后,数组内部并不是有序的。因此需要将堆化数组有序化。思想是移除根节点,并做最大堆调整的递归运算。第一次将heap[0]heap[n-1]交换,再对heap[0...n-2]做最大堆调整。第二次将heap[0]heap[n-2]交换,再对heap[0...n-3]做最大堆调整。重复该操作直至heap[0]heap[1]交换。由于每次都是将最大的数并入到后面的有序区间,故操作完后整个数组就是有序的了。
  3. 最大堆调整(Max_Heapify):该方法是提供给上述两个过程调用的。目的是将堆的末端子节点作调整,使得子节点永远小于父节点 。

动画演示:
数据结构与算法学习笔记-排序算法
算法代码(基于python):

def heap_sort(ary) :
    n = len(ary)
    first = int(n/2-1)       #最后一个非叶子节点
    for start in range(first,-1,-1) :     #构造大根堆
        max_heapify(ary,start,n-1)
    for end in range(n-1,0,-1):           #堆排,将大根堆转换成有序数组
        ary[end],ary[0] = ary[0],ary[end]
        max_heapify(ary,0,end-1)
    print(ary)
    return ary


#最大堆调整:将堆的末端子节点作调整,使得子节点永远小于父节点
#start为当前需要调整最大堆的位置,end为调整边界
def max_heapify(ary,start,end):
    root = start
    while True :
        child = root*2 +1               #调整节点的子节点
        if child > end : break
        if child+1 <= end and ary[child] < ary[child+1] :
            child = child+1             #取较大的子节点
        if ary[root] < ary[child] :     #较大的子节点成为父节点
            ary[root],ary[child] = ary[child],ary[root]     #交换
            root = child
        else :
            break

ary = [5,2,1,7,8,3,9,0,4]
heap_sort(ary)

#实验结果:0,1,2,3,4,5,7,8,9

如下图所示:
数据结构与算法学习笔记-排序算法

相关文章: