算法五:排序、搜索(算法部分)

一、排序

排序算法:能将一串数据按照某特定顺序进行排列的一种算法。

常用排序:

名称

复杂度

说明

备注

冒泡排序
Bubble Sort

O(N*N)

将待排序的元素看作是竖着排列的“气泡”,较小的元素比较轻,从而要往上浮

 

插入排序

Insertion sort

O(N*N)

逐一取出元素,在已经排序的元素序列中从后向前扫描,放到适当的位置

起初,已经排序的元素序列为空

选择排序

O(N*N)

首先在未排序序列中找到最小元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小元素,然后放到排序序列末尾。以此递归。

 

快速排序

Quick Sort

O(n *log2(n))

先选择中间值,然后把比它小的放在左边,大的放在右边(具体的实现是从两边找,找到一对后交换)。然后对两边分别使用这个过程(递归)。

 

堆排序HeapSort

O(n *log2(n))

利用堆(heaps)这种数据结构来构造的一种排序算法。堆是一个近似完全二叉树结构,并同时满足堆属性:即子节点的键值或索引总是小于(或者大于)它的父节点。

近似完全二叉树

希尔排序

SHELL

O(n1+)

0<£<1

选择一个步长(Step) ,然后按间隔为步长的单元进行排序.递归,步长逐渐变小,直至为1.

 

箱排序
Bin Sort

O(n)

设置若干个箱子,把关键字等于 k 的记录全都装入到第k 个箱子里 ( 分配 ) ,然后按序号依次将各非空的箱子首尾连接起来 ( 收集 ) 。

分配排序的一种:通过" 分配 " 和 " 收集 " 过程来实现排序。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

在实际场景中,快排用得比较多,因为它的平均效率为O(n*log2n),且很少出现O(n*n)的情况,且不像归并排序需要占用更多的空间。

 

1.冒泡排序(Bubble Sort):一种比较简单的算法

时间复杂度:稳定,平均O(n*n),最优O(n),最坏O(n*n)

辅助空间:O(1)

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

走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。

这个算法的名字由来是因为越大的元素会经由交换慢慢“浮”到数列的顶端,故名。

data_set = [9, 1, 22, 31, 45, 3, 6, 2, 11]
loop_count = 0
for j in range(len(data_set)):  # 循环n次
    # (循环n-已循环次数) + ... + (循环n-已循环次数)= (n-1) + (n-2)+.....+(n-(n-1))
    for k in range(len(data_set) - (j+1)):
        if data_set[k] > data_set[k + 1]:
            tmp = data_set[k]
            data_set[k] = data_set[k + 1]
            data_set[k + 1] = tmp
        loop_count += 1
print("loop times>>>>>>", loop_count)  # 36
print("data_set>>>>>>", data_set)

2.选择排序O(N*N):

时间复杂度:不稳定,平均O(n*n),最优O(n*n),最坏O(n*n)

辅助空间:O(1)

首先在未排序序列中找到最小元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小元素,然后放到排序序列末尾。以此递归。

示例一:

data_set = [9, 1, 22, 31, 45, 3, 6, 2, 11]
smallest_num_index = 0  # 初始列表最小值,默认为第一个
loop_count = 0
for j in range(len(data_set)):  # 循环n次
    for k in range(j, len(
            data_set)):  # (循环n-已循环次数) + ... + (循环n-已循环次数)= (n-1) + (n-2)+.....+(n-(n-1))
        if data_set[k] < data_set[smallest_num_index]:  # 当前值 比之前选出来的最小值 还要小,那就把它换成最小值
            smallest_num_index = k
        loop_count += 1
    print("smallest num is ", data_set[smallest_num_index])
    tmp = data_set[smallest_num_index]
    data_set[smallest_num_index] = data_set[j]
    data_set[j] = tmp
print("loop times>>>>>>", loop_count)  # 45
print("data_set>>>>>", data_set)

示例二:

j = 0 # 第1个元素0~倒数第2个元素n-2
# i:第2个元素1~最后一个元素n-1
# 1.用第一个元素32同剩余元素比较,得到最小元素1的索引min为3
min = 3
# 将找到的最小值1依次放在左边:即,将第0个元素和第3个元素交换
alist[0], alist[3] = alist[0], alist[0]
alist = [1, 20, 8, 32, 99, 98, 200, 299]

j = 0 # 第2个元素1~倒数第2个元素n-2
# i: 第3个元素2~最后一个元素n-1
# 2.用第二个元素20同剩余元素比较,得到最小元素8的索引min为2
min = 2
# 2.将找到的最小值1依次放在左边:即,将第1个元素和第2个元素交换
alist[1], alist[2] = alist[2], alist[1]
alist = [1, 3, 20,32, 99, 98, 200, 299]

以此类推到最后一个元素,这是选择排序,始终从右边的剩余的列表中选出最小值,放在左边

alist = [32, 20, 8, 1, 99, 98, 200, 299]
# 用代码表示:
def select_sort(alist):
    n = len(alist)
    for j in range(n-1):  # j: 0~n-2,时间复杂度:O(n-1)
        min_index = j
        for i in range(j+1, n):  # i: 1~n-1,时间复杂度比O(n-1)小,近似为n
            if alist[min_index] > alist[i]:
                min_index = i   # 时间复杂度:O(n-2) + O(n-3) + ...1等于O((n-2+1)*(n-2)/2)
        alist[j], alist[min_index] = alist[min_index], alist[j]
    print("result>>>>>", alist)
select_sort(alist)
# 总的时间复杂度:
# 1: O((n-1)*(n-2)/2)=O((n-2)*(1+(n-2)/2),约等于O(n*n)
# 2: 内层比n-2小,近似看作n,等于O(n)*O(n)

3.插入排序:

 

时间复杂度:稳定,平均O(n*n),最优O(n),最坏O(n*n)

 

辅助空间:O(1)

 

将列表分为2部分,左边为排序好的部分,右边为未排序的部分,

循环整个列表,每次将一个待排序的记录,按其关键字大小插入到前面已经排好序的子序列中的适当位置,直到全部记录插入完成为止。

插入排序非常类似于整扑克牌

 

示例一:

source = [9, 1, 22, 31, 45, 3, 6, 2, 11]
loop_count = 0
for j in range(len(source)):  # 循环n次
    current_val = source[j]  # 先记下来每次大循环走到的第几个元素的值
    position = j
    # 当前元素的左边的紧靠的元素比它大,要把左边的元素一个一个的往右移一位,给当前这个值插入到左边挪一个位置出来
    while position > 0 and source[position - 1] > current_val:
        source[position] = source[position - 1]
        position -= 1  # 只一次左移只能把当前元素一个位置 ,还得继续左移只到此元素放到排序好的列表的适当位置 为止
        loop_count += 1
    # 已经找到了左边排序好的列表里不小于current_val的元素的位置,把current_val放在这里
    source[position] = current_val
    print(source)
print("loop times>>>>>>", loop_count)  # 18
print("data_set>>>>>", source)

# 更容易理解的版本
data_set = [9, 1, 22, 9, 31, -5, 45, 3, 6, 2, 11]
loop_count = 0
for i in range(len(data_set)):
    #position  = i
    while i > 0 and data_set[i] < data_set[i - 1]:  # 右边小于左边相邻的值
        tmp = data_set[i]
        data_set[i] = data_set[i - 1]
        data_set[i - 1] = tmp
        i -= 1
        loop_count += 1
print("loop times>>>>>>", loop_count)  # 27
print("data_set>>>>>", data_set)

示例2:

默认从第1个元素32有序列的
alist = [32,      20, 8, 1, 99, 98, 200, 299]

拿第2个元素20同左边的有序序列(从后往前)依次比较,插入到比它小,比它大的元素中间;当然如果比最后一个元素大,就插入到最后,如果比第一个元素小,就插入到最前面。
alist = [20, 32,      8, 1, 99, 98, 200, 299]

依次拿第3个元素8同左边的有序序列(从后往前)依次比较,结果插入到最前面。
alist = [8, 20, 32,      1, 99, 98, 200, 299]

以此类推,拿右边的剩余的元素同左边的有序序列比较,插入到左边有序序列中。

代码表示:

alist = [32, 20, 8, 1, 99, 98, 200, 299]
def insert_sort(alist):
    n = len(alist)
    # 外层循环:循环右边列表,第2个元素20开始到最后一个元素299
    # 外层循环次数:n - 1
    for i in range(1, n):
        # 内层循环:循环左边的有序序列
        # 内层循环次数是动态的:次数为i-1
        while i > 0:  # range(i, 0, -1)
            if alist[i] < alist[i-1]:
                alist[i], alist[i-1] = alist[i-1], alist[i]
                i -= 1
            else:
                break

 

4.希尔排序(shell sort):

时间复杂度:不稳定,平均O(n*log2n)~O(n*n),最优O(n**1.3),最坏O(n*n)

辅助空间:O(1)

希尔排序(Shell Sort)是插入排序的一种。

也称缩小增量排序,是直接插入排序算法的一种更高效的改进版本。

该方法的基本思想是:先将整个待排元素序列分割成若干个子序列(由相隔某个“增量”的元素组成的)分别进行直接插入排序,然后依次缩减增量再进行排序,待整个序列中的元素基本有序(增量足够小)时,再对全体元素进行一次直接插入排序。

因为直接插入排序在元素基本有序的情况下(接近最好情况),效率是很高的,因此希尔排序在时间效率比直接插入排序有较大提高

首先要明确一下增量的取法:

      第一次增量的取法为: d=count/2;

      第二次增量的取法为:  d=(count/2)/2;

      最后一直到: d=1;

看上图观测的现象为:

        d=3时:将40跟50比,因50大,不交换。

                   将20跟30比,因30大,不交换。

                   将80跟60比,因60小,交换。

        d=2时:将40跟60比,不交换,拿60跟30比交换,此时交换后的30又比前面的40小,又要将40和30交换,如上图。

                   将20跟50比,不交换,继续将50跟80比,不交换。

        d=1时:这时就是前面讲的插入排序了,不过此时的序列已经差不多有序了,所以给插入排序带来了很大的性能提高。

示例一:

import time,random

#source = [8, 6, 4, 9, 7, 3, 2, -4, 0, -100, 99]
#source = [92, 77, 8,67, 6, 84, 55, 85, 43, 67]

source = [ random.randrange(10000+i) for i in range(10000)]
#print(source)



step = int(len(source)/2) #分组步长

t_start = time.time()


while step >0:
    print("---step ---", step)
    #对分组数据进行插入排序

    for index in range(0,len(source)):
        if index + step < len(source):
            current_val = source[index] #先记下来每次大循环走到的第几个元素的值
            if current_val > source[index+step]: #switch
                source[index], source[index+step] = source[index+step], source[index]

    step = int(step/2)
else: #把基本排序好的数据再进行一次插入排序就好了
    for index in range(1, len(source)):
        current_val = source[index]  # 先记下来每次大循环走到的第几个元素的值
        position = index

        while position > 0 and source[
                    position - 1] > current_val:  # 当前元素的左边的紧靠的元素比它大,要把左边的元素一个一个的往右移一位,给当前这个值插入到左边挪一个位置出来
            source[position] = source[position - 1]  # 把左边的一个元素往右移一位
            position -= 1  # 只一次左移只能把当前元素一个位置 ,还得继续左移只到此元素放到排序好的列表的适当位置 为止

        source[position] = current_val  # 已经找到了左边排序好的列表里不小于current_val的元素的位置,把current_val放在这里
    print(source)

t_end = time.time() - t_start

print("cost:",t_end)

 示例二:

def shell_sort(alist):
    n = len(alist)
    gap = n // 2
    # gap执行的次数:gap从n//2变化到1的循环
    while gap > 0:
        # 与普通的插入算法一样,区别:普通的插入算法步长可以看作1, 希尔算法可以看作步长为gap
        for j in range(gap, n):
            # j: [gap, gap+1, gap+2, gap+3......n-1]
            i = j
            while i > 0:
                if alist[i] < alist[i-gap]:
                    alist[i], alist[i-gap] = alist[i-gap], alist[i]
                    i -= gap
                else:
                    break
        # 逐步缩短gap步长
        gap //= 2
    return alist


print(shell_sort([92, 77, 8, 67, 6, 84, 55, 85, 43, 67]))
# 当gap没有循环,只执行一次gap=1时,是最坏情况,效率是O(n*n),这种情况相当于普通的插入算法
# gap取哪些值是最优的,需要数学算法确定。这里暂且取列表长度的对半,不断的对半,直到gap为0为止。

 


5.快速排序:

 

时间复杂度:不稳定,平均O(n*log2n),最优O(n*log2n),最坏O(n*n)

 

辅助空间:O(log2n)~O(n)

 

 

设要排序的数组是A[0]……A[N-1],首先任意选取一个数据(通常选用数组的第一个数)作为关键数据,

然后将所有比它小的数都放到它前面,所有比它大的数都放到它后面,这个过程称为一趟快速排序。

值得注意的是,快速排序不是一种稳定的排序算法,也就是说,多个相同的值的相对位置也许会在算法结束时产生变动

示例一:

loop_count = 0
def quick_sort(array, left, right):
    '''
    :param array:
    :param left: 列表的第一个索引
    :param right: 列表最后一个元素的索引
    :return:
    '''
    if left >= right:
        return
    low = left
    high = right
    key = array[low]  # 第一个值

    while low < high:  # 只要左右未遇见
        global loop_count
        loop_count += 1
        while low < high and array[high] > key:  # 找到列表右边比key大的值 为止
            high -= 1
            loop_count += 1
        # 此时直接 把key(array[low]) 跟 比它大的array[high]进行交换
        array[low] = array[high]
        array[high] = key

        while low < high and array[low] <= key:  # 找到key左边比key大的值,这里为何是<=而不是<呢?你要思考。。。
            low += 1
            loop_count += 1
            # array[low] =
        # 找到了左边比k大的值 ,把array[high](此时应该刚存成了key) 跟这个比key大的array[low]进行调换
        array[high] = array[low]
        array[low] = key

    quick_sort(array, left, low - 1)  # 最后用同样的方式对分出来的左边的小组进行同上的做法
    quick_sort(array, low + 1, right)  # 用同样的方式对分出来的右边的小组进行同上的做法
# array = [96, 14, 10, 9, 6, 99, 16, 5, 1, 3, 2, 4, 1, 13, 26, 18, 2, 45, 34, 23, 1, 7, 3, 22, 19, 2]
array = [9, 1, 22, 9, 31, -5, 45, 3, 6, 2, 11]
print("before sort:", array)
quick_sort(array, 0, len(array) - 1)
print("-------final -------")
print("loop count>>>>>>>>", loop_count)  # 37
print(array)
示例二:
快排,不再分左右序列。
版本一:
默认拿出第1个元素,在剩余元素中有两个指针,假设左边是比较小的指针,从第2个元素开始,右边是比较大的指针,从最后一个元素开始。
左、右指针同时移动。
左边比较:在序列中从左往右开始比较,假如比第1个元素小,指针继续往右走;否则指针暂停。
右边比较:在序列中从右往左开始比较,假如比第1个元素大,指针继续往左走;否则指针暂停。
当左右指针都暂停的时侯,交换这两个元素的位置,继续比较,直到左右指针重合。
左右指针重合的位置前面就是第1个元素应该存放的位置;此时,此位置左边的元素都比第1个元素小,此位置右边的元素都比第1个元素大。

然后,左、右两边的序列,再重复上面的过程,直到所有元素比较完成。

版本二:
现在左指针从第1个元素开始。右指针还是从最后一个开始。
将第1个元素的值赋值给一个中间变量mid_value,可以认为此时左指针对就应的是一个空值。
先移动右指针,同中间值比较:如果比中间值大,继续往左移;否则,将此值赋值给low指针所在的位置,同时暂停右指针。此时可以认为右指针指向为空。
当右指针暂停后,才开始左指针右移:如果比中间值小,继续右移;否则,将此值赋值给high指针所在位置,同时暂停这左指针。此时可以认为左指针指向为空。
当左右指针重合,第一次循环结束:将中间值插入到重合位置的前面,此时重合位置左边的值都比中间值小,右边的值都比中间值大。

然后,左边、右边的序列分别重复上面的过程,直到所有元素比较完成。


6.归并算法,稳定的O(n *log2(n)),但有空间上的额外开销:

时间复杂度:稳定,平均O(n*log2n),最优O(n*log2n),最坏O(n*log2n)

辅助空间:O(n)


先拆分,后合并:拆分的时侯,一直拆到1个元素为一个序列;合并的顺序刚好相反。

 






依次执行的代码:

         

归并排序的 代码:

def merge_sort(alist):
    """归并排序"""
    n = len(alist)
    if n <= 1:
        return alist
    mid = n // 2
    # left、right就归并排序后的有序部分---新的列表
    left_li = merge_sort(alist[:mid])
    right_li = merge_sort(alist[mid:])
    # 将两个有序的子序列,合并为一个新的序列
    left_pointer, right_pointer = 0, 0
    result = []
    # 比较大小部分
    while left_pointer < len(left_li) and right_pointer < len(right_li):
        if left_li[left_pointer] <= right_li[right_pointer]:
            result.append(left_li[left_pointer])
            left_pointer += 1
        else:
            result.append(right_li[right_pointer])
            right_pointer += 1
    # 比较后剩余部分
    result += left_li[left_pointer:]
    result += right_li[right_pointer:]
    return result


if __name__ == '__main__':
    li = [54, 26, 93, 17, 77, 31, 44, 55, 20]
    print(li)
    sorted_li = merge_sort(li)
    print(sorted_li)

 

 

二、搜索之 二分查找

 

 上图查找的是值为4的元素:首先找到索引的中值,即索引0 + 最后一个元素的索引8  = 8,再除以2,得到中值为索引4;然后进行比较,索引4对应的值为7,要查找的值为4,因此判断出应该在索引4的左边查找;继续以上步骤查找。

二分查找的条件:必须是排序后的顺序表,支持索引,如排序后的列表

二分查找的实现:递归查找

def binary_search(alist, item):
    """二分查找:非递归实现,无需生成新的列表,可以始终使用最初的列表alias"""
    n = len(alist)
    if n > 0:
        mid = n // 2
        if item == alist[mid]:
            return True
        elif item < alist[mid]:
            return binary_search(alist[:mid], item)
        else:
            return binary_search(alist[mid + 1:], item)
    return False


li = [17, 20, 26, 31, 44, 54, 55, 77, 93]
print(binary_search(li, 55))
print(binary_search(li, 17))
print(binary_search(li, 93))
print(binary_search(li, 200))

 

二分查找的实现:非递归,循环查找

def binary_search(alist, item):
    """二分查找:非递归实现,无需生成新的列表,可以始终使用最初的列表alias"""
    if not alist:
        return False
    n = len(alist)
    if n > 0:
        first = 0
        last = n - 1
        while first <= last:
            mid = (first + last) // 2
            if item == alist[mid]:
                return True
            elif item < alist[mid]:  # 不能包括mid元素
                last = mid - 1
            else:
                first = mid + 1
    return False


li = [17, 20, 26, 31, 44, 54, 55, 77, 93]
print(binary_search(li, 55))
print(binary_search(li, 17))
print(binary_search(li, 93))
print(binary_search(li, 200))

 

 二分查找的时间复杂度:最优O(1),最坏O(log2n)

 








 https://space.bilibili.com/248341496/video?tid=0&page=3&keyword=&order=pubdate

posted on 2018-01-06 12:29  myworldworld  阅读(223)  评论(0)    收藏  举报

导航