算法与数据结构笔记-Python实现

本文深入探讨了多种排序算法,包括插入排序、希尔排序、归并排序、快速排序和堆排序,详细阐述了它们的定义、特点、实现及优化。此外,还介绍了查找技术,如无序链表中的顺序查找。内容详实,适合学习数据结构和算法的读者。
摘要由CSDN通过智能技术生成

Menu

初级排序

插入排序

定义

  • 当前索引左边的所有元素都是有序的,但最终位置还不确定,插入排序所需的时间取决于输入中元素的初始顺序。
  • 插入排序对于实际应用中常见的某些类型的非随机数组比较有效

命题

  • 平均情况下插入排序需要N^2/4次比较与交换
  • 最坏情况下需要N^2/2次比较与交换
  • 最好情况下需要N-1次交换和0次交换
  • 插入排序需要的交换操作和数组中倒置的数量相同,需要的比较次数大于等于倒置的数量,小于等于倒置的数量加上数组的大小再减一
    • 每一次交换都改变了两个顺序颠倒的元素的位置,相当于减少了一对倒置,导致数量为0时,排序完成。

实现

def insertsort(a):
    for i in range(1,len(a)):
        # 将a[i] 插入到 a[i-1],a[i-2],a[i-3]...中
        for j in range(i,0,-1):
            if a[j]<a[j-1]:
                a[j],a[j-1]=a[j-1],a[j]
  • 第一个for循环将每一个右边的元素插入进左边的有序区域
  • 第二个for循环针对被插入后的左边区域进行检索交换

优化

  • 在内循环(第二层循环)中,将较大的元素都向右移动而不总是交换两个元素,这样访问数组的次数就能减半

希尔排序

定义

  • 基于插入排序的快速的排序算法
    • 对于大规模乱序数组,插入排序很慢,因为只能一点一点地把元素从一端移动到另一端
  • 希尔排序为了加快速度简单地改进了插入排序,交换不相邻的元素来实现数组的局部排序,并最终用插入排序将局部有序的数组排序
  • 一个h有序数组就是h个互相独立的有序数组编织在一起组成的数组
  • 希尔排序的实现时通过在h子数组中将每个元素交换到比它大的元素之前去,只需要在插入排序的代码中将移动元素的距离由1改为h。

特点

  • 希尔排序可以用于大型数组,它对任意排序的数组表现也很好
  • 对于中等大小的数组,可以直接选用希尔排序,因为代码量很小,也不需要使用额外的内存空间

实现

def ShellSort(a):
    N = len(a)
    h = 1
    while (h < N/3):
        h = 3*h + 1 # (1,4,13,40,121,364)
    while (h >= 1):
        # 将数组变为h有序
        for i in range(h,N):
            # 执行插入排序
            for j in range(i,h-1,-h):
                if(a[j] < a[j-h]):
                    a[j],a[j-h]=a[j-h],a[j]
        h = h // 3

归并排序

  • 归并: 将两个有序的数组归并成一个更大的有序数组

定义

  • 归并排序将数组递归地分成两半分别排序,然后将结果归并起来
  • 归并排序保证排序任意长度为N的数组时间运算复杂度为NlogN

原地归并

  • 将子数组a[lo…mid]和a[mid+1…hi]归并成一个有序的数组并存放在a[lo…hi]
  • 实现:
    • 如下代码中将所有的元素复制到aux[]中,然后再归并回a[]
    • 方法在归并时(第二个for循环)进行了四个条件判断:
        1. 左半边用尽,则取右半边的元素
        1. 右半边用尽,则取左半边的元素
        1. 右半边的当前元素小于左半边的当前元素,取右半边的元素
        1. 右半边的当前元素大于等于左半边的当前元素,取左半边的元素
public static void merge(Comparable[] a, int lo, int mid, int hi)
{ // 将a[lo..mid] 和 a[mid+1..hi] 归并
 int i = lo, j = mid+1;
 for (int k = lo; k <= hi; k++) // 将a[lo..hi]复制到aux[lo..hi] 
    aux[k] = a[k];
 for (int k = lo; k <= hi; k++) // 归并回到a[lo..hi] 
    if (i > mid) a[k] = aux[j++]; 
    else if (j > hi ) a[k] = aux[i++]; 
    else if (less(aux[j], aux[i])) a[k] = aux[j++]; 
    else a[k] = aux[i++]; 
}

自顶向下的归并排序

  • 实现
def sort(a)
    aux = a.copy()
    mergesort(a, 0, len(a)-1)

def mergesort(list a, int lo, int hi):
    if lo <= hi:
        return
    mid = lo + (hi - lo)/2
    mergesort(a, lo, mid)
    mergesort(a, mid+1, hi)
    merge(a, lo, mid, hi)
  • 优化:
    • 对小规模子数组使用插入排序
    • 测试数组是否已经有序

自底向上的归并排序

  • 首先进行两两归并(把每个元素想象成一个大小为1的数组)
  • 然后进行四四归并
  • 然后八八归并,一直进行下去
  • 在每一轮的归并中,最后一次归并的第二个子数组可能比第一个子数组小,否则就是相同,而下一轮中子数组的大小需要翻倍。

命题

  • 归并排序是一种渐进最优的基于比较排序的算法,运算复杂度NlogN

快速排序

特点

  • 原地排序,只需要一个很小的辅助栈,运算复杂度NlogN
  • 主要缺点:非常脆弱,在实现的时候要非常小心避免低劣的性能
  • 当切分不平衡时,这个程序可能会相当低效

基本实现

def quicksort(a):
    if len(a)==0:
        return a
    x = a[0]
    left = [i for i in a[1:] if i<x]
    right = [i for i in a[1:] if i>=x]
    return quicksort(left) + [x] + quicksort(right)

改进实现

    1. 对于小数组,快速排序比插入排序慢,因为递归,快速排序的在小数组中也会调用自己
    • 针对此,在排序小数组时应该切换到插入排序
    1. 三取样切分,使用子数组的一小部分元素的中位数切分数组

优先队列

特点

  • 支持两种操作,删除最大元素和插入元素

二叉堆

  • 堆有序,当一棵二叉树的每个节点都大于等于他的两个子结点时,它被称为堆有序

  • 在堆有序的二叉树中,每个节点都小于等于它的父节点,从任意结点向上,我们都能得到一列非递减的元素,从任意节点向下,我们都能得到一列非递增的元素

  • 堆的算法

    • 用长度为N+1的私有数组pq[]来表示一个大小为N的堆,不使用pq[0],而是将元素放在pq[1]至pq[N]
  • 完全二叉树只用数组而不需要指针就可以表示,具体方法是将二叉树的结点按照层级顺序放入数组中,根结点放在位置1,子节点在位置2和3,而子节点的子节点在位置4,5,6,7,以此类推

    • 所以,在一个堆中,位置为k的结点的父结点的位置是k/2,而他的两个自节点的位置则是2k 2k+1
    • 如此,从a[k]向上移动一层即是 k = k/2,向下一层则是k = 2k 或 2k+1
  • 用他们可以实现对数级别的插入元素和删除最大元素的操作

比较与交换的代码实现 less(),exch
def less(int i, int j):
  return pq[i] < pq[j]

def exch(int i, int j):
  pq[i],pq[j]=pq[j],pq[i]
由下至上的堆有序化(上浮)
  • 如果堆的有序状态因为某个结点变得比它的父结点更大而被打破
  • 那么我们需要通过交换它和它的福街店来修复堆
  • 将这个结点不断地向上移动直到我们遇到了一个更大的父结点
上浮代码实现 swim()
  • 只要k大于1,并且k的父结点小于k,则交换其位置
def swim(int k):
  while (k>1 and less(k/2, k)):
    exch(k/2, k)
    k = k/2
由上至下的堆有序化(下沉)
  • 如果堆中某个结点比它的两个子结点,或其中一个更小了,就需要交换他和他的两个子结点中较大的那一个来恢复有序
  • 将结点向下移动知道它的子节点都比它小或是达到了堆的底部
下沉代码实现 sink()
def sink(int k):
  while(2*k <= N): # N = len(dp)
    j = 2*k
    if (j<N and less(j,j+1)):  # 判断子结点左边的大还是右边的大,如果是右边的大就进行 加一
      j += 1
    if (not less(k,j)): # 如果父结点大于子节点,就break,结束
      break
    exch(k,j) #否则进行交换,让k和j的值交换,然后把指针移动到j上
    k = j
二叉堆的操作
  • 插入元素
    • 把新元素加到数组末尾
    • 增加堆的大小并让这个新元素上浮到合适的位置
  • 删除最大元素
    • 从数组顶端删去最大的元素并将数组的最后一个元素放到顶端
    • 减小堆的大小并让这个元素下沉到合适的位置
代码实现
  def insert(v):
    N = N + 1
    pq[N] = v
    swim(N)
  
  def delMax():
    max = pq[1]
    exch(1,N)  #将空出来的1和最后一个元素进行交换
    N = N-1
    pq[N+1] = None
    sink(1)   #下沉第一个元素让其到合适的位置
    return max
特点
  • 对于一个含有N元素的基于堆的有线队列,插入元素操作只需要不超过lgN+1次操作
  • 删除最大元素的操作需要不超过2lgN次

多叉堆

  • 基于数组表示的三叉堆与二叉堆相似
  • 位置k的结点大于等于位置为3k-1, 3k, 3k+1的结点,小于等于(k+1)/3的结点

堆排序

实现
  • 将所有元素插入到一个查找最小元素的优先队列
  • 然后反复调用删除最小元素的操作来将它们按顺序删去,用无序数组实现的优先队列这么做相当于做了一次选择排序
  • 用基于堆的优先队列这么做等同于堆排序 :经典而优雅
堆排序的两个阶段
    1. 在堆的构造阶段中,我们将原始数组重新组织安排进一个堆中;
    1. 在下沉排序阶段,我们从堆中按递减顺序取出所有元素并得到排序结果
    • 堆的构造:
      • 可以在NlogN的时间内将N个元素构造成一个堆
        • 只需从左至右遍历,用swim保证指针左侧的元素都是堆有序的完全树即可
        • 更高效的办法是,从右至左用sink构造子堆
          • 这个过程中的遍历只需要从数组的中间处开始,因为其后的部分是最后一个层级
          • 下沉操作N个元素构造堆只需要 少于2N次比较少于N次交换
堆排序实现
  • 首先使用for循环构造堆
  • 然后使用while循环将最大的元素a[1]和a[N]交换并且修复堆,如此重复直到堆变空
  • 由于原始的sink操作是针对a[1]-a[N]进行的,所以将比较与exchange过程中的下标都减一就可以实现针对a[0]-a[N-1]的排序
def HeapSort(a):
  N = len(a)
  for k in range(N//2,0,-1): #从数组的中间开始往左遍历 构造堆 ,因为右半边是最下面一层子节点
    sink(a, k ,N)
  while (N > 1):
    exch(a, 1, N)
    N = N -1 
    sink(a, 1, N)
    #首先将最大的元素移动到最末端,
    #然后将堆的下标减一,让剩下的堆的最大移动到第一位
    #重复这个操作我们就得到了递增序列

def sink(pq, k, n):
  while(2*k <= n): # N = len(dp)
    j = 2*k
    if (j<n and pq[j-1]<pq[j+1-1]):  # 判断子结点左边的大还是右边的大,如果是右边的大就进行 加一
      j += 1
    if (not pq[k-1]<pq[j-1]): # 如果父结点大于子节点,就break,结束
      break
    exch(pq,k,j) #否则进行交换,让k和j的值交换,然后把指针移动到j上
    k = j

def exch(pq, i, j):
  pq[i-1],pq[j-1]=pq[j-1],pq[i-1]
优化实现:
  • 先下沉后上浮:
    • 大多数在下沉排序期间重新插入堆的元素会被直接加入到堆底,我们可以通过不检查元素是否到达正确位置来节省时间。
    • 在下沉过程中总是直接提升较大的子节点直至到达堆底,然后再让元素上浮到正确的位置
    • 这可以减少一半的比较次数,接近了归并排序,但需要额外的空间,在需要操作字符串或其他键值较长的类型的时候可以考虑使用
  • 堆排序实际上可以同时最优地利用空间和时间的方法,但现代系统地许多应用很少使用他,因为他无法利用缓存。

各种排序算法的性能特点

  • 快速排序是最快的通用排序算法
    • 快速排序内循环中的指令很少,而且可以利用缓存。运行时间的增长数量级为 cNlgN 这里的c比其他线性对数级别的排序算法的都要小
    • 在使用三向切分之后,快速排序对于实际应用中可能出现的某些分布的输入就可以变成线性级别的了,而其他的排序算法仍然需要线性对数时间
  • 所以,在大多数情况下,快速排序是最佳选择

排序应用

  • 找出重复元素:
      1. 先使用排序将数组有序化
      1. 使用遍历记录连续出现的重复元素
    • 时间复杂度:NlgN

查找

  • 使用符号表这个词来描述一张抽象的表格
  • 符号表有时被称为字典,有时被叫做索引(键和值)
  • 有序符号表的一些API:
    • min(), max(), floor(key)小于等于key的最大键, ceiling(key)大于等于key的最小键
    • rank(key)小于key的键的数量, select(k)排名为k的键
    • deleteMin(), deleteMax()
    • size(lo, hi) [lo…hi]之间键的数量
    • keys(lo,hi) [lo…hi]之间的所有键,已排序

无序链表中的顺序查找

定义

  • 符号表中使用的数据结构一个简单的选择是链表,每个节点存储一个键值对。
    • 顺序查找:在查找中我们一个个地顺序遍历符号表中的所有键并使用equals()来寻找匹配的键
  • 基础实现与简单API
class LinkedList():
    class Node():
        def __init__ (self,_key,_val,next):
            self.key = _key
            self.value = _val
            self.next =next

    def __init__(self):
        self.head = self.Node(None,None,None)
        self.size = 0

    def size(self):
        return self.size

    def get(self,_key):
        node = self.head
        while node:
            if(node.key==_key):
                return node.value
            node = node.next
        return None

    def put(self,_key,_value):
        node = self.head
        while node:
            if(node.key == _key):
                node.value = _value
                return 
            node = node.next
        self.head = self.Node(_key, _value,self.head)
        self.size += 1

    def delete(self,_key):
        node = self.head
        while node:
            if(node.key == _key):
                node.value = None
                return

    def keys(self):
        node = self.head
        all_keys = []
        while node:
            if node.value and node.key:
                all_keys.append(node.key)
            node = node.next
        all_keys.sort()
        return all_keys
  • 在含有N对键值的基于无序链表的符号表中,未命中的查找和插入操作都需要N次比较。命中的查找在最坏情况下需要N次比较。
  • 向一个空表中插入N个不同的键需要N^2/2次比较

Updating

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值