经典算法之排序算法

排序

讲解使用什么语言作为demo?其实很多人讲解算法都使用的是java或者是C++,因为他们认为算法本身就是性能的质变,所以语言也要用尽可能快的语言(和我原来的思想很像),对于这个问题现在我是这样理解的,算法的核心是思想,你需要明白的也是思想,至于什么语言将来你使用的时候针对你的编程环境使用就好了,学习贵在学会,不要太纠结细节。

这一章节主要讲排序算法,那么我们就先了解各种常用的排序算法的使用场景有哪些?下面这个图片应该是记录比较全面的。

sort_0.jpg

首先我们要定义的标准然后实现的方式来继承。

class Sort(object):
    @abc.abstractmethod
    def sort(self, array_list):
        raise NotImplementedError

插入排序

代码展示

class InsertSort(Sort):

    def insert_sort(self, arr):
        for i in range(1, len(arr)):
            key = arr[i]
            j = i - 1
            while j >= 0 and key < arr[j]:
                arr[j + 1] = arr[j]
                j -= 1
            arr[j + 1] = key
        return arr

    def sort(self, array_list):
        arr = self.insert_sort(array_list)
        return arr

以上是插入排序的python代码,他的大致的思想是不断的往arr这个数组里插入数据,插入的数据也就是代码中的key变量,从后往前插入,如果比当前的元素大,就把当前元素后移,知道找到比key小的就停下来,将元素放到这里。

复杂度分析

然后我们来分下下这个算法的上下限制,最快的时候就是`O(n)`了,原数组基本有序,遍历一遍不需要做元素的移动,最差的当然就是倒序的了,每次都要移动所有的元素,所以整体的复杂度为`O(n^2)`,

选择排序

代码展示

class SelectSort(Sort):
    def __select_min_idx(self, arr):
        min = sys.maxint
        min_idx = 0
        for idx in range(len(arr)):
            if arr[idx] < min:
                min = arr[idx]
                min_idx = idx
        return min_idx

    def sort(self, array_list):
        for i in range(len(array_list)):
            idx = self.__select_min_idx(array_list[i:])
            swap(array_list, i, idx + i)
        return array_list

选择排序的思路更加粗暴和简单,我就是每次都转转变成找极值的问题,例如上面的代码,我每次都找最小的元素和当前的遍历元素进行交换,这样我能保证遍历完数组的时候就是整体数组有序的时候。

复杂度分析

选择排序是性能比较差的排序,每次找极值的时候都需要遍历整个元素,所以算法复杂度很好分析,就是O(n2)O(n^2),并且没得商量,和原始数据是否有效无关。

堆排序

堆是一种非线性结构,(本篇随笔主要分析堆的数组实现)可以把堆看作一个数组,也可以被看作一个完全二叉树,通俗来讲堆其实就是利用完全二叉树的结构来维护的一维数组

按照堆的特点可以把堆分为大顶堆和小顶堆

大顶堆:每个结点的值都大于或等于其左右孩子结点的值

小顶堆:每个结点的值都小于或等于其左右孩子结点的值

sort_1.jpg

先来看看上面的图片,是堆排序的过程。

class HeapSort(Sort):
    def heapify(self, arr, n, i):
        largest = i
        l = 2 * i + 1
        r = 2 * i + 2
        if l < n and arr[i] < arr[l]:
            largest = l
        else:
            largest = i
        if r < n and arr[largest] < arr[r]:
            largest = r
        if largest != i:
            swap(arr, largest, i)
            self.heapify(arr, n, largest)

    def __build_heap(self, arr):
        n = len(arr)/2 - 1
        for i in range(n, -1, -1):
            self.heapify(arr, n, i)

    def sort(self, array_list):
        self.__build_heap(array_list)
        n = len(array_list)
        for i in range(n - 1, 0, -1):
            swap(array_list, i, 0)
            self.heapify(array_list, i, 0)
        return array_list

以上是堆排序算法的过程,我来看看算法的流程,首先对array_list进行建堆,所谓建立堆就是不允许一个父节点小于他的子节点而已,进行self.heapify操作,对于构建好的堆一直取堆顶的元素也就是sort方法,就称为堆排序,这里涉及到一个递归,就是每次有一个节点调整以后,相应的会不断移动和这个元素相关的元素。这里的核心方法是** heapify**方法,这个方法主要起到的作用是调整堆的机构,大家和把这段代码拿到自己机器上调试,看看每次的运行结果。

复杂度分析

堆排序的核心操作是heapify,在__build_heap的过程中,这个函数会被调用`\frac{n}{2}-1`次,而实际的排序中,这个方法会被调用n-1次,总共被调用`\frac{3n}{2}-2`次,堆本质是一个树,树的高度是`logn`,然后还有一个递归的操作,每次校正从堆底到堆等最多也就是`logn`,整个过程比较次数不会操作$$2n$次,所以整体算法的复杂度就是次,所以整体算法的复杂度就是$O(nlogn)$$

快速排序

class QuickSort(Sort):
    def qsort(self, arr, left, right):
        if left >= right:
            return
        i = left
        j = right
        key = arr[left]
        while i < j:
            while i < j and key <= arr[j]:
                j -= 1
            arr[i] = arr[j]
            while i < j and key > arr[i]:
                i += 1
            arr[j] = arr[i]
        arr[i] = key
        self.qsort(arr, left, i - 1)
        self.qsort(arr, i + 1, right)

    def sort(self, array_list):
        self.qsort(array_list, 0, len(array_list) - 1)
        return array_list

还有一种写法,可能更好理解一些。

def quickSort(arr):
    if len(arr) <= 1:
        return arr
    else:
        smaller = []
        equal = []
        greater = []
        pivot = arr[random.randint(0, len(arr) - 1)]
        for elem in arr:
            if elem < pivot:
                smaller.append(elem)
            elif elem == pivot:
                equal.append(elem)
            else:
                greater.append(elem)
    return quickSort(smaller) + equal + quickSort(greater)

快速排序的思想也比较简单,主要是找到一个基准,让基准左边的元素比基准小,右边的元素比基准大,qsort这个方法就是移动元素来找到咱们说的基准,第一个while循环里想要找到的是比基准小的元素,所以一旦发现比基准大的就直接左移,找到以后就记下这个元素,保存到arr[i]这里, 然后就是从左边找比基准大的元素的了,同理也是按照上文说的方法找,找到以后,就要交换这两个元素(arr[i],arr[j]),然后找到相应的key的位置,就递归的对左边和右边的元素都做这样的操作。

复杂度分析

这是个比较有趣的算法,因为如果数据是逆序的时候,这个算法的复杂度会退化到`O(n^2)`,当然平均状况下会是`O(nlogn)`

桶排序

这算法是比较少见的不基于比较的排序算法

class BucketSort(Sort):

    def sort(self, array_list):
        n = max(array_list) + 1
        arr = [0 for i in range(n)]
        for item in array_list:
            arr[item] += 1
        array_list = list()
        for i in range(n):
            while arr[i] != 0:
                array_list.append(i)
                arr[i] -= 1
        return array_list

堆排序算法要求你的数据是范围是确定的,然后申请一个这么大的数组,将元素的下标元素打上标记,虽然这个算法不基于比较,但是是构建到一个有序数组的基础上的。

复杂度分析

能够看出这个算法的最坏复杂度和平均复杂度都是`O(n)`

归并排序

上文介绍的的基本上都不需要借助于外部存储空间,但是归并算法是需要借助外部空间的。

class MergeSort(Sort):
    def merge(self, arr1, arr2):
        arr = []
        h = j = 0
        while j < len(arr1) and h < len(arr2):
            if arr1[j] < arr2[h]:
                arr.append(arr1[j])
                j += 1
            else:
                arr.append(arr2[h])
                h += 1
        if j == len(arr1):
            for i in arr2[h:]:
                arr.append(i)
        else:
            for i in arr1[j:]:
                arr.append(i)
        return arr

    def sort(self, array_list):
        if len(array_list) <= 1:
            return array_list
        middle = len(array_list) / 2
        left = self.sort(array_list[:middle])
        right = self.sort(array_list[middle:])
        return self.merge(left, right)

上面的代码就是一个完整的归并排序,其中merge方法就是合并两个有序list的操作,在sort方法里,递归的排序数组的一部分,上文说的占用额外的空间就是merge中的数组arr,最后合并最后两个left和right数组就是我们要的有序数组。

算法复杂度

首先确空间复杂度是`O(n)`,归并排序的归并算法也就是merge算法复杂度是`O(n)`,然后递归执行这一过程,最后的算法复杂度是`O(nlogn)`.

不知道大家注意到没有,一个序列中多个归并是完全独立的,这个也就是是说这个算法是比较好完成并行化的,以下可以看下博主这篇文章

# 面试 
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×