算法课 Week2 笔记

第一部分:栈、队列和背包

跟上周一样,讨论一个数据结构同样从定义API开始,定义的是范型可迭代的API,这样可以支持任意一个数据类型,而不用根据不同的数据类型来定义和实现不同的API。

泛型:Generics

目的:使得编写的代码可以被不同类型的对象重用。

定义:只需要在类名后面增加

Stack<String> stack = new Stack<String>(); // 一个字符串栈
Queue<Date> queue = new Queue<Date>(); // 一个Date队列

迭代:Iterators

背包、队列和栈都是集合类数据结构,这种结构的基本操作之一就是能够使用Java的foreach语句通过迭代遍历并处理集合中的每个元素。

迭代器都是泛型的。

实现迭代的好处:无需知晓类的实现细节也可以使用迭代。

实现迭代的步骤:

  1. 在程序的开头加上import java.util.Iterator;

  2. 在要迭代的类的声明中加入implements Iterable<T>

  3. 在类中添加一个方法iterator()并返回一个迭代器Iterator<T>,迭代器可以叫任意名字,只要它实现了hasNext()和next()方法即可,迭代器的类声明中要加入implements Iterator<T>(注意和上面的Iterable区分)。虽然接口定义里面还指定了一个remove()方法,但是书中只在foreach中使用迭代器,所以remove中内容为空。

    public Iterator<T> iterator()
    { return new DieDaiQiIterator(); }
    private class DieDaiQiIterator implements Iterator<T>
    {
    private int i = N;
    
    public boolean hasNext() { return i > 0; }
    public T next() { return a[--i];    }
    public void remove() {}
    }

Iterable和Iterator的区分:

  • Iterable是有能够返回迭代器(Iterator)方法的一种类
  • Itertator是具有hasNext()和next()方法的类

自动装箱:autoboxing

原始数据类型和引用类型(封装类型)的区别:

  1. Java的封装类型都是原始数据类型对应的引用类型
  2. 声明
    • 声明int a,a自动等于0
    • 声明Integer a,a自动等于null
    • 两者可以通过自动装箱/拆包来互相转化
      • 自动装箱:自动将原始类型转化为封装类型
      • 自动拆箱:自动将封装类型转化为原始类型
  3. double是原始类型,Double是封装类型,为了能够给原始类型提供额外的方法,比如ArrayList里的add必须add的是对象,而int不是对象,所以要使用Integer

后进先出结构

public class Stack<T> implements Iterable<T>

Stack() // 创建空栈
void push(T item) // 压栈
T pop() // 弹出一个元素
boolean isEmpty() // 判空
int size() // 返回栈的大小

栈的实现:

  • 定容数组栈

  • 可以动态调整大小的数组栈:

    • API中要包含一个可以检测栈是否已满的方法:isFull(),和一个调整大小的方法:resize,将当前的栈的元素复制到新的栈中
    • push操作的时候检查一下栈是否满了,满了的话就将创建一个大小是原数组2倍的新数组,调用resize,将原数组元素复制到新数组中
    • 为了避免数组中过分空闲,pop操作时,检测栈的大小是否小于数组的四分之一,小于的话就创建一个大小是原栈的一半的新栈,并调用resize,将原数组中的元素复制到新数组中
    • 为什么是四分之一而不是二分之一的时候调整栈的大小?
    • 因为二分之一就调整的话,要是连续出现push和pop操作,就要不停地调整数组大小,划不来
    • pop时要注意避免对象游离,对象游离是指虽然你已经把元素从数组中弹出了,但是Java不知道,还会继续保留对已弹出元素的引用,即便是这个元素再也不会被访问了。所以要弹出元素后,将弹出元素的地方设为null来覆盖无用引用,并使系统可以回收它的内存 ,确保充分利用内存。
  • 链栈:

    • 时间复杂度:最坏情况下每个操作都是常数时间

    • 空间复杂度:N个项的栈用了将近40N个字节

  • 关于用顺序数组实现的可调大小栈和链栈之间的选择:

    • 如果需要每次操作都很快,而不会被其中一次卡住,用链栈
    • 如果只在乎总的时间,可以用顺序栈,因为顺序栈的总时间(平均算下来)比链栈小

栈的应用:

  • 可以用两个栈实现一个队列
  • 算数表达式求值——Dijkstra的双栈算数表达式求值算法

背包

背包其实就是个只能装东西不能倒东西的包,目的就是帮助用例收集元素并迭代遍历所有收集到的元素。

背包的实现只要将栈的push改为add并去掉pop就行。

public class Bag<T> implements Iterable<T>

Bag() // 创建一个空背包
void add(T item) // 添加一个元素
boolean isEmpty() // 判空
int size() // 返回背包元素数量

队列

先进先出的结构,书中也使用了链表来实现。

public class Queue<T> implements Iterable<T>

Queue() // 创建空队列
void enqueue(T item) // 入队
T dequeue() // 出队
boolean isEmpty() // 判空
int size() // 返回队列大小

使用队列的一个主要原因是它可以保存元素的同时保存它们的相对顺序。

第二部分:初级排序算法

评估算法的性能:

  • 排序成本模型
    • 计算比较和交换的数量
    • 对于不交换的算法,计算访问数组的次数
  • 内存开销:
    • 原地排序:除了函数调用所需的栈和固定数目的实例变量外,无需额外内存
    • 需要额外内存来存储另一份数组副本

Comparable接口

sort方法怎么做到在不知道主键是什么类型的情况下来对任意类型的数据进行比较的?

答:设置一个回调机制,主函数将需要排序的对象数组传给sort(),sort()按需调用对象的compareTo()函数,只要这个对象实现了Comparable接口。

实现Comparable接口只用在类声明后面加上implements Comparable,再在类中实现一个compareTo()方法来定义目标类型对象的自然次序就可以了,大小顺序通过返回+1,-1,0来定义。

选择排序

每次都找最小的元素和前面的交换。

特点:

  1. 运行时间和输入无关。
  2. 数据移动是最少的。

插入排序

将未排序的一个个插到已排序的数组中的适当位置。

特点:

  1. 运行时间取决于输入元素的初始顺序。如果一个数据接近有序,或者部分有序那么用插入排序会特别快。
    • 三种典型的部分有序:
      1. 数组中每个元素都离它的最终位置不远
      2. 一个有序的大数组接一个小数组
      3. 数组中只有几个元素的位置不正确
  2. 插入排序需要的交换操作和数组中倒置的数量相同,需要的比较次数大于等于倒置的数量,小于等于倒置的数量加上数组的大小减一
  3. 适合小规模数组。

希尔排序

希尔排序就是改进的插入排序。

插入排序慢的原因在于它只会交换相邻元素,每次只移动了一步,因此元素只能一步步挪到正确的位置。

希尔排序快在可以大幅移动元素,虽然这样导致了它的不稳定性。

基本思想:

  • 使数组中任意间隔h的元素是有序的。h很大就可以将元素易到很远的地方。
    • h的选择:一般用3x+1,即13,4,1
    • h由大变小,最后变成1的时候就是排序完成
    • h = 1的时候其实就是插入排序
  • 如果一个序列是经过h排序的,用另一个值k进行k排序,该序列仍然是h有序的。

排序的应用——洗牌

给每张牌安排一个随机数,将这些随机数排序就达到了洗牌的效果。

但是这样要耗费一次排序的代价,实际上不用那么麻烦,而且可以在线性的时间内完成洗牌,具体是:随机从已洗牌的牌组中选择一和未洗牌的牌组交换位置,洗牌的牌组大小加1,未洗的减1,直到洗完。

递增i,再生成一个随机数r,交换它们。

可以用来在线性时间内得到一个随机均匀的序列。

int r = StdRandom.uniform(i+1)

另一个应用:Convex Hull 凸包

猜你喜欢

转载自blog.csdn.net/dmbjzhh/article/details/80949306