算法学习:常用排序方法

排序是我们的日常开发中经常会遇到的需求,例如,在商品的列表页面,我们可以根据各种维度(销量、价格、人气等)对商品的展示顺序进行改变。

所以,对各个排序的性能的了解也是基础且重要的。我们先对排序这一块进行一个整体的把握。

  • 冒泡排序
  • 选择排序
  • 插入排序
  • 希尔排序
  • 堆排序
  • 归并排序
  • 快速排序

关于排序的具体过程,本文没有利用图示来具体说明,但是在代码中对每一步执行加上了一些 log,有兴趣的可以复制代码到自己的 Playgroud 中运行查看。

冒泡排序

基本思想

两两比较相邻记录,如果反序则交换,直到没有反序的记录为止。

代码实现

func bubbleSort(_ items: [Int]) -> [Int] {
    var arr = items
    for i in 0..<arr.count {
        print("第\(i + 1)轮冒泡:")
        var j = arr.count - 1

        while j > i {
            if arr[j-1] > arr[j] {
                print("\(arr[j-1]) > \(arr[j]): 进行交换")
                arr.swapAt(j-1, j)
            } else {
                print("\(arr[j-1]) <= \(arr[j]): 不进行交换")
            }

            j -= 1
        }
        print("第\(i + 1)轮冒泡结束")
        print("当前结果为:\n\(arr)\n")
    }
    return arr
}
复制代码

根据上面的实现过程,可以发现如果数组是类似于 [2, 1, 3, 4, 5] 的话,两次冒泡之后,数组就已经调整为 [1, 2, 3, 4, 5]。此时,再执行后面的循环其实是多余了,所以我们在发现冒泡循环没有进行交换的话,说明排序就已经完成了。下面我们可以将代码优化一下:

func bubbleSort_v2(_ items: [Int]) -> [Int] {
    var arr = items

    var flag = true

    for i in 0..<arr.count {
        print("第\(i + 1)轮冒泡:")

        if !flag {
            return []
        }

        flag = false

        var j = arr.count - 1

        while j > i {
            if arr[j-1] > arr[j] {
                print("\(arr[j-1]) > \(arr[j]): 进行交换")
                arr.swapAt(j-1, j)

                flag = true
            } else {
                print("\(arr[j-1]) <= \(arr[j]): 不进行交换")
            }

            j -= 1
        }
        print("第\(i + 1)轮冒泡结束")
        print("当前结果为:\n\(arr)\n")
    }
    return arr
}
复制代码

选择排序

基本思想

每一次从待排序的数据元素中选择最小(或最大)的一个元素放在已排好序列的末尾,直到所有元素排完为止。

代码实现

func selectSort(_ items: [Int]) -> [Int] {
    var arr = items
    
    for i in 0..<arr.count {
        print("第\(i+1)轮选择,x选择下标的范围为\(i)----\(arr.count)")
        var j = i + 1
        var minValue = arr[i]
        var minIndex = i
        
        // 寻找无序部分中最小值
        while j < arr.count {
            if minValue > arr[j] {
                minValue = arr[j]
                minIndex = j
            }
            j = j + 1
        }
        
        print("在后半部分乱序数列中,最小值为:\(minValue),下标为:\(minIndex)")
        // 与无序表中的第一个值交换,让其成为有序表中的最后一个值
        if minIndex != i {
            arr.swapAt(minIndex, i)
        }
        print("本轮结果为:\(arr)\n")
    }
    
    return arr
}
复制代码

插入排序

基本思想

每一步将一个待排序的记录,插入到前面已经排好序的有序序列中去,直到插完所有元素为止,最终可以得到一个有序表。

代码实现

func insertSort(_ items: [Int]) -> [Int] {
    var arr = items
    
    for i in 0..<arr.count {
        print("第\(i)轮插入:")
        print("要选择插入的值为:\(arr[i])")
        var j = i
        
        while j > 0 {
            if arr[j] < arr[j-1] {
                arr.swapAt(j, j-1)
                
                j -= 1
            } else {
                break
            }
        }
        print("插入的位置:\(j)")
        print("本轮插入完毕,插入结果为:\n\(arr)\n")
    }
    
    return arr
}
复制代码

希尔排序

基本思想

其实希尔排序是插入排序的升级版,希尔排序根据其排序的特点又叫做缩小增量排序。希尔排序的大体步骤就是先将无序序列按照一定的步长(增量)分为几组,分别将这几组中的数据通过插入排序的方式将其进行排序。然后缩小步长(增量)分组,然后将组内的数据再次进行排序。直到增量为 1 位置。经过上述这些步骤,我们的序列就是有序的了。其实上述的插入排序就是增量为 1 的希尔排序。

代码实现

func hillSort(_ items: [Int]) -> [Int] {
    var arr = items
    
    var step: Int = arr.count / 2
    
    while step > 0 {
        print("步长为\(step)的插入排序开始:")
        for i in 0..<arr.count {
            var j = i + step
            
            while j >= step && j < arr.count {
                if arr[j] < arr[j-step] {
                    arr.swapAt(j, j-step)
                    
                    j = j - step
                } else {
                    break
                }
            }
        }
        print("步长为\(step)的插入排序结束")
        print("本轮排序结果为:\(arr)\n")
        step = step / 2      // 缩小步长
    }
    
    return arr
}
复制代码

堆排序

准备知识

是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。

我们对堆中的结点按层进行编号,将这种逻辑结构映射到数组中就是下面这个样子。

大顶堆: arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2]

小顶堆: arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2]

基本思想

将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点。将其与末尾元素进行交换,此时末尾就为最大值。然后将剩余 n-1 个元素重新构造成一个堆,这样会得到 n 个元素的次小值。如此反复执行,便能得到一个有序序列了。

基本步骤

  1. 将无序序列构建成一个堆,根据升序降序需求选择大顶堆或小顶堆。
  2. 将堆顶元素与末尾元素交换,将最大元素"沉"到数组末端。
  3. 重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整及交换步骤,直到整个序列有序。

代码实现

// 构建小顶堆的层次遍历序列
func heapCreate(items: inout Array<Int>) {
    var i = items.count
    while i > 0 {
        heapAdjast(items: &items, startIndex: i - 1, endIndex: items.count)
        
        i -= 1
    }
}

// 对小顶堆j的局部进行调整, 使其该节点的所有父类符合小顶堆的特点
func heapAdjast(items: inout Array<Int>, startIndex: Int, endIndex: Int) {
    let temp = items[startIndex]
    var fatherIndex = startIndex + 1    //父节点下标
    var maxChildIndex = 2 * fatherIndex //左孩子下标
    
    while maxChildIndex <= endIndex {
        if maxChildIndex < endIndex && items[maxChildIndex-1] < items[maxChildIndex] {
            maxChildIndex = maxChildIndex + 1
        }
        
        if temp < items[maxChildIndex-1] {
            items[fatherIndex-1] = items[maxChildIndex-1]
        } else {
            break
        }
        fatherIndex = maxChildIndex
        maxChildIndex = 2 * fatherIndex
    }
    items[fatherIndex-1] = temp
}

func heapSort(_ items: [Int]) -> [Int] {
    var arr = items
    
    var endIndex = arr.count - 1
    
    print("原始堆")
    print(arr)
    
    // 创建小顶堆,其实就是讲arr转换成小顶堆层次的遍历结果
    heapCreate(items: &arr)
    
    print(arr)
    
    print("堆排序开始")
    while endIndex > 0 {
        // 将小顶堆的定点与小顶堆的最后一个值进行交换
        arr.swapAt(endIndex, 0)
        
        endIndex -= 1    // 缩小小顶堆的范围
        
        // 对交换后的小顶堆进行调整,使其重新成为小顶堆
        print("重新调整")
        heapAdjast(items: &arr, startIndex: 0, endIndex: endIndex + 1)
        print("调整后的结果如下")
        print(arr)
    }
    
    return arr
}
复制代码

归并排序

基本思想

归并排序是利用归并的思想实现的排序方法,该算法采用经典的分治策略。

将需要排序的数组分解为一个个单独的子元素,两两比较后将其合并为一个有序数组,不断重复比较及合并的过程,直至合并之后的有序数组的元素数量与原数组相同时,则排序结束。

代码实现

// 合并两个数组
func mergeArray(firstList: Array<Int>, secondList: Array<Int>) -> Array<Int> {
    var resultList: Array<Int> = []
    var firstIndex = 0
    var secondIndex = 0
    
    // 拿出两数组第一个元素,比较之后取小的插入新数组
    while firstIndex < firstList.count && secondIndex < secondList.count {
        if firstList[firstIndex] < secondList[secondIndex] {
            resultList.append(firstList[firstIndex])
            firstIndex += 1
        } else {
            resultList.append(secondList[secondIndex])
            secondIndex += 1
        }
    }
    
    // 将第一个数组剩下的元素插入新数组
    while firstIndex < firstList.count {
        resultList.append(firstList[firstIndex])
        firstIndex += 1
    }
    
    // 将第二个数组剩下的元素插入新数组
    while secondIndex < secondList.count {
        resultList.append(secondList[secondIndex])
        secondIndex += 1
    }
    
    return resultList
}

func mergeSort(_ items: [Int]) -> [Int] {
    let arr = items
    
    // 将数组中的每一个元素放入一个数组中
    var tempArray: Array<Array<Int>> = []
    for item in arr {
        var subArray: Array<Int> = []
        subArray.append(item)
        tempArray.append(subArray)
    }
    
    // 对这个数组中的数组进行合并,直到合并完毕为止
    while tempArray.count != 1 {
        print(tempArray)
        var i = 0
        while i < tempArray.count - 1 {
            print("将\(tempArray[i])与\(tempArray[i+1])合并")
            tempArray[i] = mergeArray(firstList: tempArray[i], secondList: tempArray[i+1])
            print("合并结果为:\(tempArray[i])\n")
            tempArray.remove(at: i + 1)
            i = i + 1
        }
    }
    
    return tempArray.first!
}
复制代码

快速排序

基本思想

快速排序(Quick Sort)使用分治法策略。

选择一个基准数,通过一趟排序将要排序的数据分割成独立的两部分;其中一部分的所有数据都比另外一部分的所有数据都要小。然后,再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。

快速排序流程:

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

代码实现

/// 将数组以第一个值为准分为两部分,前半部分比该值要小,后半部分比该值要大
///
/// - parameter list: 要二分的数组
/// - parameter low:  数组的下界
/// - parameter high: 数组的上界
///
/// - return: 分界点
func partition(list: inout Array<Int>, low: Int, high: Int) -> Int {
    var low = low
    var high = high
    let temp = list[low]
    
    while low < high {
        while low < high && list[high] >= temp {
            high -= 1
        }
        list[low] = list[high]
        
        while low < high && list[low] <= temp {
            low += 1
        }
        list[high] = list[low]
        
        print("====low==\(low)======high==\(high)")
    }
    
    list[low] = temp
    print("=====temp==\(temp)")
    print(list)
    return low
}

/// 快速排序
///
/// - parameter list: 要排序的数组
/// - parameter low:  数组的下界
/// - parameter high: 数组的上界
func quickSortHelper(list: inout Array<Int>, low: Int, high: Int) {
    if low < high {
        let mid = partition(list: &list, low: low, high: high)
        
        quickSortHelper(list: &list, low: low, high: mid - 1)
        quickSortHelper(list: &list, low: mid + 1, high: high)
    }
}

func quickSort(_ items: Array<Int>) -> Array<Int> {
    var list = items
    quickSortHelper(list: &list, low: 0, high: list.count - 1)
    print("快速排序结束")
    return list
}
复制代码

Swift排序用法

在Swift源代码中,sort 函数采用的是一种内审算法(IntroSort)。它由堆排序、插入排序、快速排序三种算法构成,依据输入的深度相应选择最佳的算法来完成。

// 以升序排列为例,array 数组可改变
array.sort

// 以降序排列为例,array 数组不可改变
newArray = array.sorted(by: >)
复制代码

总结

排序方法 时间复杂度 辅助空间
冒泡排序 O(n²) O(1)
选择排序 O(n²) O(1)
插入排序 O(n²) O(1)
希尔排序 与增量选取策略有关 O(1)
堆排序 O(nlogn) O(1)
归并排序 O(nlogn) O(n)
快速排序 O(nlogn) O(logn)-O(n)

转载于:https://juejin.im/post/5cf4955df265da1b7401e6da

猜你喜欢

转载自blog.csdn.net/weixin_33860528/article/details/91478235