Skip to content

Latest commit

 

History

History

08-sorting

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
# 排序算法

# 如何分析一个“排序算法”?
### 排序算法的执行效率
1.最好情况、最坏情况、平均情况时间复杂度
   - 有序度不同的数据,对于排序的执行时间会有影响的
   - 不同时间复杂度情况下,对应的原始数据

2.时间复杂度的系数、常数 、低阶
   - 在对同一阶时间复杂度的排序算法性能对比的时候,就要把系数、常数、低阶也考虑进来

3.比较次数和交换(或移动)次数
   - 两种操作,一种是元素比较大小,另一种是元素交换或移动

### 排序算法的内存消耗
- 空间复杂度:原地排序(Sorted in place),特指空间复杂度是 O(1) 的排序算法

### 排序算法的稳定性
- 如果待排序的序列中存在值相等的元素,经过排序之后,相等元素之间原有的先后顺序不变
- 之所以要考察稳定性:实际排序的不是整数,而是对象,按照某个特定的属性值排序,但需要保持其它属性值的顺序


# 一、比较类排序
## 冒泡排序(Bubble Sort):
将大的元素**向上冒泡**,移动到数组右边
- 每次冒泡操作都会对相邻的两个元素进行比较,看是否满足大小关系要求。如果不满足就让它俩互换。
- 一次冒泡会让至少一个元素移动到它应该在的位置,重复 n 次,就完成了 n 个数据的排序工作。
```python 
def bubble_sort(nums):
    n = len(nums)
    for i in range(n):

        flag = False                # 提前退出冒泡循环的标志
        for j in range(n-i-1):
            if nums[j] > nums[j+1]:
                nums[j], nums[j+1] = nums[j+1], nums[j]
                flag = True
        
        if not flag:                # 没有数据交换,提前退出
            break
    return nums
```
- 空间复杂度 O(1),稳定的排序算法,
- 最好情况下O(1)复杂度,最坏情况下数据倒序排列O(n^2)时间复杂度
- 平均时间复杂度:
    - 数据的有序度:有序元素对`i<j,a[i]<=a[j]`
        - 倒序数组,有序对为0
        - 完全有序数组,如 `[1,2,3,4,5]`,则有序度是 `n*(n-1)/2`,**满有序度**
        - 逆序度 = 满有序度 - 有序度
        - 冒泡排序每交换一次,有序度加 1,总交换次数为逆序度,即**`n*(n-1)/2`-初始有序度**
    - 因此考虑最好及最差,然后平均,平均情况下,操作数`n*(n-1)/4`次交换,时间复杂度 O(n^2)
    
## 插入排序(Insertion Sort):    
保持待排序元素**左边所有元素有序**,找到合适的位置,将该元素插入到该位置处
```python
def insertion_sort(nums):
    n = len(nums)
    for i in range(1, n):
        curr = nums[i]
        j = i - 1
        while j >= 0 and nums[j] > curr:
            nums[j + 1] = nums[j]
            j -= 1
        nums[j + 1] = curr
    return nums
```

- 空间复杂度是 O(1),稳定的排序算法
- 最好情况下O(n)复杂度,最坏情况下数据倒序排列O(n^2)时间复杂大,平均 O(n^2)

## 选择排序(Selection Sort):
数据分为已排序区间和未排序区间,每次从未排序区间中找到最小的元素,将其放到已排序区间的末尾
```python
def selection_sort(nums):
    n = len(nums)
    for i in range(n - 1):
        min_index = i
        for j in range(i + 1, n):
            if nums[j] < nums[min_index]:
                min_index = j
        nums[i], nums[min_index] = nums[min_index], nums[i]
    return nums
```
- 空间复杂度是 O(1),平均时间复杂度 O(n^2)
- 但是**不稳定的排序算法**:[5,8,5,2,9],第一次找到最小值 2,arr[0]=5 会和 2 交换位置,最终两个 5 的相对位置改变了

## 冒泡 vs 插入
```python
# 冒泡排序中数据的交换操作:
if a[j] > a[j+1]:
    a[j], a[j+1] = a[j+1], a[j]
   
# 插入排序中数据的移动操作:
if a[j] > val:  
    a[j+1] = a[j] 
```
- 冒泡排序的数据交换要比插入排序的数据移动要复杂,冒泡排序需要交换两个数,而插入排序只需要一次赋值
- 相对来说,插入排序更快


## 归并排序(Merge Sort):
```python
def merge(s1, s2, s):
    i = j = 0
    while i + j < len(s):  # KEY:关键的判断逻辑!!!
        if j == len(s2) or (i < len(s1) and s1[i] < s2[j]):
            s[i + j] = s1[i]
            i += 1
        else:
            s[i + j] = s2[j]
            j += 1
    return s

def merge_sort(s):
    if len(s) < 2:
        return s
    mid = len(s) // 2
    s1 = merge_sort(s[:mid])
    s2 = merge_sort(s[mid:])
    return merge(s1, s2, s)
```
- 稳定的排序算法;不管是最好情况、最坏情况,还是平均情况,时间复杂度都是 O(nlogn)
- 但是不是原地排序,空间复杂度是 O(n)


## 快速排序(Quick sort):
数组中取标杆 pivot,将小元素放 pivot 左边,大元素放右侧;然后递归的分别对左、右两部分快排
```python
def partition(nums, left, r):
    pivot = nums[left]
    i = left + 1  # KEY: i-left 表示小于等于 pivot 元素的个数
    for j in range(left + 1, r):
        if nums[j] < pivot:
            nums[j], nums[i] = nums[i], nums[j]
            i += 1
    nums[left], nums[i - 1] = nums[i - 1], nums[left]
    return i - 1


def quick_sort(nums, l, r):
    if l >= r:
        return
    j = partition(nums, l, r)
    quick_sort(nums, l, j)
    quick_sort(nums, j + 1, r)
    return nums
```
- 快速排序并不是一个稳定的排序算法,原地排序算法
- 时间复杂度都可以做到 O(nlogn),只有在极端情况下,才会退化到 O(n^2)

## 归并 vs 快排
归并的过程是由下至上,先处理子问题,然后合并;快排相反,由上至下,先分区,再处理子问题



# 二、非比较类排序:
线性排序(Linear sort),时间负责度 O(n);不基于比较的排序算法,都不涉及元素之间的比较操作:桶排序、计数排序、基数排序



##  桶排序(Bucket sort)

将要排序的数据分到几个有序的桶里,每个桶里的数据再单独进行排序。
时间复杂度:
- 如果要排序的数据有 n 个,把它们均匀地划分到 m 个桶内,每个桶里就有 k=n/m 个元素。
- 每个桶内部使用快速排序,时间复杂度为 O(k * logk)。
- m 个桶排序的时间复杂度就是 O(m * k * logk),整个桶排序的时间复杂度就是 O(n*log(n/m
))。
- 当桶的个数 m 接近数据个数 n 时,log(n/m) 就是一个非常小的常量,这个时候桶排序的时间复杂度接近 O(n)。



限制:

- 要排序的数据需要很容易就能划分成 m 个桶,并且,桶与桶之间有着天然的大小顺序

- 数据在各个桶之间的分布是比较均匀的,极端情况如果数据都被划分到一个桶里,那就退化为 O(nlogn) 的排序算法了。

桶排序比较适合用在外部排序中。所谓的外部排序就是数据存储在外部磁盘中,数据量比较大,内存有限,无法将数据全部加载到内存中。

```python
def bucket_sort(nums):
    min_, max_ = min(nums), max(nums)
    
    bucket_count = (max_ - min_) // 5 + 1  # 默认每个桶的容量为 5
    buckets = [[] for i in range(bucket_count)]
    
    for num in nums:
        ind = (num - min_) // 5  # 将每个数映射到特定的桶中,每个桶有顺序
        buckets[ind].append(num)
    
    res = []
    for buck in buckets:
        if bool(buck):
            res.extend(insertion_sort(buck))  # 对每个桶中的数插入排序
    return res
```







## 计数排序(Counting sort)

计数排序其实是桶排序的一种特殊情况。当要排序的 n 个数据,所处的范围并不大的时候,比如最大值是 k,把数据划分成 k 个桶。每个桶内的数据值都是相同的,省掉了桶内排序的时间。

```python
def counting_sort(nums):
    max_num = max(nums)  # O(N) 时间可以完成
    bucket = [0] * (max_num + 1)
    for i in range(len(nums)):
        bucket[nums[i]] += 1
    
    sortedIndex = 0
    for j in range(max_num + 1):
        while bucket[j] > 0:
            nums[sortedIndex] = j
            sortedIndex += 1
            bucket[j] -= 1
    return nums
```