数据结构与算法笔记之排序算法(一)

文章目录

  • 排序算法
    • 冒泡排序
    • 选择排序
    • 插入排序
    • 归并排序
      • 归并排序
      • 逆序对问题
      • 求数组的小和
    • 快速排序
      • partition问题
      • 荷兰国旗问题
      • 快速排序第一版
      • 快速排序第二版
    • 堆排序
      • 堆的插入
      • pop操作
      • 代码实现
    • 计数排序
    • 基数排序

排序算法

参考:https://www.cnblogs.com/onepixel/articles/7674659.html
排序的稳定性:如果排序数组具有相同值的元素,并且排序前排序后相对位置不变,那么我们称这种排序为稳定排序。稳定排序的作用一般在对类进行排序的时候,一个类有一些其他属性,对于这些属性存在顺序要求的话就需要用稳定排序。

冒泡排序

两个指针i,j,i代表i之前都是有序的,arr[j]与arr[j-1]进行对比,假如arr[j]

def bubble_sort(arr):
    for i in range(len(arr)):
        for j in range(len(arr) - 1, i - 1, -1):
            if arr[j] < arr[j - 1]:
                arr[j], arr[j - 1] = arr[j - 1], arr[j]

时间复杂度:O(n^2)
空间复杂度:O(1)
稳定性:稳定

选择排序

两个指针i,j,i之前都是排序完成的,选择i之后最小的那个数与i进行交换,以此类推。

def select_sort(arr):
    for i in range(len(arr)):
        min_value = arr[i]
        min_index = i
        for j in range(i, len(arr)):
            if min_value > arr[j]:
                min_value = arr[j]
                min_index = j
        arr[i], arr[min_index] = arr[min_index], arr[i]

时间复杂度:O(n^2)
空间复杂度:O(1)
稳定性:稳定

插入排序

两个指针i,j,i之前都是排序完成的,选择i插入到之前其对应位置,以此类推。

def insert_sort(arr):
    for i in range(1, len(arr)):
        j = i
        while j > 0:
            if arr[j] < arr[j - 1]:
                arr[j], arr[j - 1] = arr[j - 1], arr[j]
                j -= 1
            else:
                break

时间复杂度:O(n^2)
空间复杂度:O(1)
稳定性:稳定

归并排序

归并排序

其本质采用分治策略,步骤为先分后合。时间复杂度O(NlogN),空间复杂度O(N)
例子:
要将[5,3,4,2,1]排序

  1. 分:[5,3,4]、[2,1]
  2. 分:[5,3]、[4]、[2,1]
  3. [5,3]合并:[3,5]、[4]、[2,1]
  4. [4]合并:[3,5]、[4]、[2,1]
  5. [3,5]、[4]合并:[3,4,5]、[2,1]
  6. [2,1]合并:[3,4,5]、[1,2]
  7. 全合并:[1,2,3,4,5]
    代码:
def merge_sort(arr):
    divide(arr, 0, len(arr) - 1)
    return arr


def divide(arr, left, right):
    if left >= right:
        return
    mid = int((left + right) / 2)
    divide(arr, left, mid)
    divide(arr, mid + 1, right)
    merge(arr, left, mid, right)


def merge(arr, left, mid, right):
    help_arr = []
    i = left
    j = mid + 1
    while i <= mid and j <= right:
        if arr[i] <= arr[j]:
            help_arr.append(arr[i])
            i += 1
        else:
            help_arr.append(arr[j])
            j += 1
    while i <= mid:
        help_arr.append(arr[i])
        i += 1
    while j <= right:
        help_arr.append(arr[j])
        j += 1
    for k in range(left, right + 1):
        arr[k] = help_arr[k - left]

时间复杂度:O(nlogn)
空间复杂度:O(n)
稳定性:稳定

逆序对问题

逆序对-如果存在l 例如:数组 <2,3,8,6,1> 的逆序对为:<2,1> < 3,1> <8,1> <8,6> <6,1> 共5个逆序对。
该问题最好的解决方法就是使用归并排序,左边逆序个数+右边逆序个数+merge逆序个数。
暴力解决:

def reverse_pair_force(arr):
    cnt = 0
    for i in range(len(arr)):
        for j in range(i, len(arr)):
            if arr[j] < arr[i]:
                cnt += 1
    return cnt

归并解决:

def reverse_pair(arr):
    reverse_pair_res = []
    cnt = divide(arr, 0, len(arr) - 1, reverse_pair_res)
    return cnt, reverse_pair_res


def divide(arr, left, right, res):
    if left >= right:
        return 0
    mid = int((left + right) / 2)
    cnt_left = divide(arr, left, mid, res)
    cnt_right = divide(arr, mid + 1, right, res)
    cnt_merge = merge(arr, left, mid, right, res)
    return cnt_left + cnt_right + cnt_merge


def merge(arr, left, mid, right, res):
    help_arr = []
    i = left
    j = mid + 1
    cnt = 0
    while i <= mid and j <= right:
        if arr[i] <= arr[j]:
            help_arr.append(arr[i])
            i += 1
        else:
            cnt += mid - i + 1
            for k in range(i, mid + 1):
                res.append((arr[j], arr[k]))
            help_arr.append(arr[j])
            j += 1
    while i <= mid:
        help_arr.append(arr[i])
        i += 1
    while j <= right:
        help_arr.append(arr[j])
        j += 1
    for k in range(left, right + 1):
        arr[k] = help_arr[k - left]
    return cnt

reverse_pair_res是逆序对
cnt是逆序对的个数

求数组的小和

题目:在一个数组中,每一个数左边比当前数小的数累加起来,叫做这个数组的小和。求一个数组的小和。
例子:[1,3,4,2,5]

  • 1左边比1小的数,没有;
  • 3左边比3小的数,1;
  • 4左边比4小的数,1、3;
  • 2左边比2小的数,1;
  • 5左边比5小的数,1、3、4、2;
  • 所以小和为1+1+3+1+1+3+4+2=16
    其实就是求逆序对的另一个版本,也就是归并排序降序版本的逆序对值的总和
    暴力解法:
def min_sum_force(arr):
    cnt = 0
    for i in range(len(arr)):
        for j in range(i, len(arr)):
            if arr[j] > arr[i]:
                cnt += arr[i]
    return cnt

归并解决:

def min_sum(arr):
    cnt = divide(arr, 0, len(arr) - 1)
    return cnt


def divide(arr, left, right):
    if left >= right:
        return 0
    mid = int((left + right) / 2)
    cnt_left = divide(arr, left, mid)
    cnt_right = divide(arr, mid + 1, right)
    cnt_merge = merge(arr, left, mid, right)
    return cnt_left + cnt_right + cnt_merge


def merge(arr, left, mid, right):
    help_arr = []
    i = left
    j = mid + 1
    cnt = 0
    while i <= mid and j <= right:
        if arr[i] > arr[j]:
            help_arr.append(arr[i])
            i += 1
        else:
            for k in range(i, mid + 1):
                cnt += arr[k]
            help_arr.append(arr[j])
            j += 1
    while i <= mid:
        help_arr.append(arr[i])
        i += 1
    while j <= right:
        help_arr.append(arr[j])
        j += 1
    for k in range(left, right + 1):
        arr[k] = help_arr[k - left]
    return cnt

快速排序

partition问题

问题描述:给定一个数组arr和一个数num,让大于这数的数字放在数组的右边,小于等于这个数字放在数组的左边。 要求:时间复杂度O(n),额外空间复杂度O(1)
解决思路:
快慢指针法,定义2个指针i=0,j=0,i代表i左边的数字小于等于num,j代表遍历到的数字。

  • 如果arr[j]<=num,swap(arr[i],arr[j]),i++,j++
  • 如果arr[j]>num,j++

当j==len(arr)时停止

例子:
arr=[1,7,3,5,7,2] num=5

  • i=0,j=0,arr[j]<=num,swap(arr[i],arr[j]),arr=[1,7,3,5,7,2],i++,j++
  • i=1,j=1,arr[j]>num,j++
  • i=1,j=2,arr[j]<=num,swap(arr[i],arr[j]),arr=[1,3,7,5,7,2],i++,j++
  • i=2,j=3,arr[j]<=num,swap(arr[i],arr[j]),arr=[1,3,5,7,7,2],i++,j++
  • i=3,j=4,arr[j]>num,j++
  • i=3,j=5,arr[j]<=num,swap(arr[i],arr[j]),arr=[1,3,5,2,7,7],i++,j++

代码:

def partition(arr, num):
    i, j = 0, 0
    while j < len(arr):
        if arr[j] <= num:
            arr[i], arr[j] = arr[j], arr[i]
            i += 1
        j += 1

荷兰国旗问题

问题描述:给定一个数组arr和一个数num,让大于这数的数字放在数组的右边,等于这个数字放在数组的中间,小于这个数字放在数组的左边。 要求:时间复杂度O(n),额外空间复杂度O(1)

解决思路:
快慢指针法,定义3个指针i=0,j=0,k=len(arr)-1,i代表i左边的数字小于num,j代表遍历到的数字,k代表k右边的数字大于num。

  • 如果arr[j]
  • 如果arr[j]==num,j++
  • 如果arr[j]>num,swap(arr[k],arr[j]),k–

当j>k的时候停止

例子:
arr=[1,7,3,5,7,5] num=5

  • i=0,j=0,k=5,arr[j]
  • i=1,j=1,k=5,swap(arr[k],arr[j]),arr=[1,5,3,5,7,7],k–
  • i=1,j=1,k=4,arr[j]==num,j++
  • i=1,j=2,k=4,arr[j]
  • i=2,j=3,k=4,arr[j]==num,j++
  • i=2,j=4,k=4,arr[j]>num,swap(arr[i],arr[j]),arr=[1,3,5,5,7,7],k–

代码:

def partition2(arr, num):
    i, j, k = 0, 0, len(arr) - 1
    while j <= k:
        if arr[j] < num:
            arr[j], arr[i] = arr[i], arr[j]
            j += 1
            i += 1
        elif arr[j] > num:
            arr[j], arr[k] = arr[k], arr[j]
            k -= 1
        else:
            j += 1

快速排序第一版

第一版本快速排序首先选择数组的最后一个元素做num,进行partition,再分别对左右两边进行partition,依次递归,代码如下:

def quick_sort1(arr):
    def partition(arr, left, right, num):
        i, j = left, left
        while j <= right:
            if arr[j] <= num:
                arr[i], arr[j] = arr[j], arr[i]
                i += 1
            j += 1
        return i - 1

    def qsort(arr, left, right):
        if left >= right:
            return
        num = arr[right]
        mid = partition(arr, left, right, num)
        qsort(arr, left, mid - 1)
        qsort(arr, mid + 1, right)

    if not arr:
        return

    qsort(arr, 0, len(arr) - 1)

快速排序第二版

第一版本快速排序存在一个问题就是假如有重复元素,就需要做多次判断,为了解决这个问题,可以使用荷兰国旗问题进行partition,代码如下:

def quick_sort2(arr):
    def partition2(arr, left, right, num):
        i, j, k = left, left, right
        while j <= k:
            if arr[j] < num:
                arr[j], arr[i] = arr[i], arr[j]
                j += 1
                i += 1
            elif arr[j] > num:
                arr[j], arr[k] = arr[k], arr[j]
                k -= 1
            else:
                j += 1
        return i - 1, k + 1

    def qsort(arr, left, right):
        if left >= right:
            return
        num = arr[right]
        i, k = partition2(arr, left, right, num)
        qsort(arr, left, i)
        qsort(arr, k, right)

    if not arr:
        return

    qsort(arr, 0, len(arr) - 1)

时间复杂度:O(nlogn)
空间复杂度:O(1)
稳定性:不稳定

堆排序

堆实际上是一个完全二叉树在数组层面的映射关系

  • 对于大根堆,该完全二叉树具有父节点永远大于子节点的特性。

  • 对于小根堆,该完全二叉树具有父节点永远大于子节点的特性。

  • 完全二叉树:设二叉树的深度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第 h 层所有的结点都连续集中在最左边。

节点间的下标关系

  • 父节点和左节点的关系:2*i+1
  • 父节点和右节点的关系:2*i+2
  • 子节点和父节点的关系:(i-1)/2

堆的插入

这里以小根堆为例
将新插入的数组放入队列尾部,循环进行up操作,up操作如下:

  1. 将该数与父节点做比较,如果小于父节点与父节点进行交换
  2. 如果大于父节点或者没有父节点了,停止循环。

例子:
假如heap中存在数组[3,5,7,9]
此时想插入一个4

  • 将4放入数组尾部[3,5,7,9,4]
  • 4与父节点5进行比较,发现大于4,那么进行交换[3,4,7,9,5]
  • 4再次与父节点3进行比较,发现小于3,终止循环,堆最终顺序为[3,4,7,9,5]

pop操作

这里以小根堆为例
将堆数组尾部的值copy到堆数组的第一个,堆长度index–,循环进行heapify操作,heapify操作如下:

  1. 从根节点开始,与子节点做比较,选择值最小的子节点进行交换
  2. 如果父节点全部小于子节点或者不存在子节点时,退出循环

例子:
假如heap中存在数组[3,4,7,9,5],pop弹出3输出,将5放到头部[5,4,7,9]

  • 5与子节点4,7比较,发现4小于5,与4进行交换[4,5,7,9]
  • 5再与子节点9进行比较,发现5小于9,退出循环,最终堆为[4,5,7,9]

代码实现

class Heap:
    def __init__(self):
        self.heap = []
        self.index = 0

    def is_empty(self):
        return self.index == 0

    def up(self, up_index):
        father_index = int((up_index - 1) / 2)
        while father_index >= 0 and self.heap[up_index] < self.heap[father_index]:
            self.heap[up_index], self.heap[father_index] = self.heap[father_index], self.heap[up_index]
            up_index = father_index
            father_index = int((up_index - 1) / 2)

    def insert(self, num):
        if self.index >= len(self.heap):
            self.heap.append(num)
        else:
            self.heap[self.index] = num
        self.up(self.index)
        self.index += 1

    def pop(self):
        if self.index == 0:
            return
        num = self.heap[0]
        self.index -= 1
        self.heap[0] = self.heap[self.index]
        self.heapify(0)
        return num

    def heapify(self, heapify_index):
        while 1:
            son_index_left = 2 * heapify_index + 1
            son_index_right = 2 * heapify_index + 2
            if son_index_right < self.index:
                min_index = son_index_left if self.heap[son_index_left] < self.heap[
                    son_index_right] else son_index_right
            elif son_index_left < self.index:
                min_index = son_index_left
            else:
                break
            if self.heap[heapify_index] > self.heap[min_index]:
                self.heap[heapify_index], self.heap[min_index] = self.heap[min_index], self.heap[heapify_index]
                heapify_index = min_index
            else:
                break

对于python而言,已经存在现成的堆的数据结构了,叫优先队列。

from queue import PriorityQueue


# 使用heapq实现优先队列
# 定义一个可比较对象
class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __str__(self):
        return str(self.__dict__)

    def __lt__(self, other):  # operator <
        return self.age > other.age


pq = PriorityQueue()
pq.put(Person("red", 11))
pq.put(Person("blue", 9))
pq.put(Person("black", 22))
pq.put(Person("white", 25))
pq.put(Person("yellow", 17))

while pq.qsize() != 0:
    person = pq.get()
    print(person)

因为底层默认调用<比较的所以如果想要做大根堆,就需要重写__lt__方法。
结果:

{'name': 'white', 'age': 25}
{'name': 'black', 'age': 22}
{'name': 'yellow', 'age': 17}
{'name': 'red', 'age': 11}
{'name': 'blue', 'age': 9}

有没有发现,最后打印出来的是有顺序的数组。这就是堆排序

时间复杂度:O(nlogn)
空间复杂度:O(n)
稳定性:不稳定

计数排序

先确定排序中值的范围,比如值范围为0-10,那么开辟一个长度为10的数组a,遍历原来数组,统计各个值出现的次数,比如3出现了3次就在数组a索引为3的值记为3,然后遍历数组a进行排序。
但这个排序存在缺点就是只能对整数进行排序,而且一旦数组中的值变大,开辟的空间也要很大,严重占用内存。

def count_sort(arr):
    min_value = min(arr)
    max_value = max(arr)
    len_vaue = max_value - min_value + 1
    bucket = [0 for i in range(len_vaue)]
    for i in arr:
        bucket[i - min_value] += 1
    print(bucket)
    for j in range(len(bucket)):
        cnt = 0
        for k in range(bucket[j]):
            print(j+ min_value)
            arr[cnt] = j + min_value
            cnt += 1

基数排序

基数排序(radix sorting)将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。 然后 从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。

from queue import Queue


def radix_sort(arr):
    bucket = [Queue() for i in range(10)]
    my_arr = [str(i) for i in arr]
    max_len = 0
    for i in my_arr:
        if max_len < len(i):
            max_len = len(i)
    for i, v in enumerate(my_arr):
        my_arr[i] = (max_len - len(v)) * "0" + v
    index = max_len - 1
    while index >= 0:
        for i in my_arr:
            bucket[int(i[index])].put(i)
        cnt = 0
        for j in bucket:
            while not j.empty():
                my_arr[cnt] = j.get()
                cnt += 1
        index -= 1
    return [int(i) for i in my_arr]

你可能感兴趣的:(数据结构与算法,数据结构,算法)