算法(第四版) | 基础编程模型、数据抽象、包、队列和栈部分内容

本文详细介绍了Java编程的基础,包括原始数据类型、表达式、语句、数组和数据抽象。讲解了声明、赋值、条件、循环、调用和返回等语句类型,并深入探讨了数组的创建、初始化和使用。此外,还阐述了数据抽象的概念,以及对象创建和几何对象的抽象数据类型。文章还涵盖了集合类如包、队列和栈的实现,特别是泛型、自动装箱和拆箱,以及链表作为数据结构在栈和队列实现中的应用。
摘要由CSDN通过智能技术生成

1.1 基础编程模型

1.1.1 Java程序的基本结构

一段Java程序(类)或者是一个静态方法(函数)库,或者定义了一个数据类型,会用到下面七种语法,它们是Java语言的基础,也是大多数现代语言所共有的。

  • 原始数据类型: 它们在计算机程序中精确地定义整数、浮点数和布尔值等。它们的定义包括取值范围和能够对相应的值进行的操作,它们能够被组合为类似于数学公式定义的表达式。
  • 语句: 语句通过创建变量并对其赋值、控制运行流程或者引发副作用来进行计算。我们会使用六种语句:声明、赋值、条件、循环、调用和返回。
  • 数组: 数组是多个同种数据类型的值的集合。
  • 静态方法: 静态方法可以封装并重用代码,使我们可以用独立的模块开发程序。
  • 字符串: 字符串是一连串的字符,Java内置了对它们的一些操作。
  • 标准输入/输出: 标准的输入输出是程序与外界联系的桥梁
  • 数据抽象: 数据抽象封装和重用代码,使我们可以定义费原始数据类型,进而支持面向对象编程。

1.1.2 原始数据类型与表达式

数据类型就是一组数据和对其所能进行的操作的集合。

  • 整型: 其算数运算符(int);
  • 双精度实数类型: 其算数运算符(double);
  • 布尔型: 它的值{true,false}及其逻辑操作(boolean);
  • 字符型: 它的值是你能够输入的英文字母数字字符和符号(char);
  • 64位整数,及其算术运算符(long);
  • 16位整数,及其算术运算符(short);
  • 16位字符,及其算术运算符(char);
  • 8位整数,及其算术运算符(byte);
  • 32位单精度实数,及其算术运算符(float)

1.1.2.2 类型转换

如果不会损失信息,数值会被自动提升为高级的数据类型。例如,在表达式1+2.5中,1会被转换为浮点数1.0,表达式的值也为double值3.5。转换指的是在表达式中把类型名放在括号里将其后的值转换为括号中的类型。例如,(int)3.7的值是3而(double)3的值是3.0。需要注意的是将浮点型转换为整型将会截断小数部分而非四舍五入,在复杂的表达式中的类型转换可能会很复杂,应该小心并尽量少使用类型转换,最好是在表达式中只使用同一类型的字面量和变量。

1.1.2.3 比较

下列运算符能够比较相同数据类型的两个值并产生一个布尔值:相等(==)、不等(! =)、小于(<)、小于等于(<=)、大于(>)和大于等于(>=)。这些运算符被称为混合类型运算符,因为它们的结果是布尔型,而不是参与比较的数据类型。结果是布尔型的表达式被称为布尔表达式。我们将会看到这种表达式是条件语句和循环语句的重要组成部分。

1.1.3 语句

Java程序是由语句组成的。语句能够通过创建和操作变量、对变量赋值并控制这些操作的流程来描述运算。语句通常会被组织成代码段,即花括号中的一系列语句。

  • 声明语句: 创建某种类型的变量并用标识符为其命名。
  • 赋值语句: 将(由表达式产生的)某种类型的数值赋予一个变量。Java还有一些隐式赋值的语法可以使某个变量的值相对于前值发生变化,例如将一个整型值加1.
  • 条件语句: 能够简单地改变执行流程——根据指定的条件执行两个代码段之一。
  • 循环语句: 更彻底地改变执行流程——只要条件为true就不断地反复执行代码段中的语句。
  • 调用和返回语句: 和静态方法有关,是改变执行流程和代码组织的另一种方式。

程序就是由一系列声明、赋值、条件、循环、调用和返回语句组成的。 一般来说代码的结构都是嵌套的:一个条件语句或循环语句的代码段中也能包含条件语句或循环语句。例如,rank()中的while循环就包含一个if语句。接下来,我们逐个说明各种类型的语句。

1.1.3.1 声明语句

声明语句将一个变量名和一个类型在编译时关联起来。Java需要我们用声明语句指定变量的名称和类型。这样,我们就清楚地指明了能够对其进行的操作。Java是一种强类型的语言。因为Java编译器会检查类型的一致性(例如,它不会允许布尔类型和浮点类型的变量相乘)。变量可以声明在第一次使用之前的任何地方——一般我们在首次使用该变量的时候声明它。变量的作用域就是定义它的地方,一般由相同代码段中声明之后的所有语句组成。

1.1.3.2 赋值语句

赋值语句将(由一个表达式定义的)某个数据类型的值和一个变量关联起来。在Java中,当我们写下 c=a+b 时,我们表达的不是数学等式,而是一个操作,即令变量c的值等于变量a的值与变量b的值之和。当然,在赋值语句执行后,从数学上来说c的值比如会等于a+b,但语句的目的是改变c的值(如果需要的话)。赋值语句的左侧必须是单个变量,右侧可以是能够得到相应类型的值的任意表达式。

表1.1.3总结了各种Java语句及其示例与定义。

1.1.4 数组

数组能够顺序存储相同类型的多个数据。除了存储数据,我们也希望能够访问数据。访问数组中的某个元素的方法是将其编号然后索引。如果我们有N个值,它们的编号则为0至N-1。这样对于0到N-1之间任意的i,我们就能够在Java代码中用a[i]唯一地表示第i+1个元素的值。在Java中这种数组被称为一维数组。

1.1.4.1 创建并初始化数组

在Java程序中创建一个数组需要三步:

  • 生命数组的名字和类型;
  • 创建数组;
  • 初始化数组元素。

在声明数组时,需要指定数组的名称和它含有的数据的类型。在创建数组时,需要指定数组的长度(元素的个数)。例如,在以下代码中,“完整模式”部分创建了一个有N个元素的double数组,所有的元素的初始值都是0.0。第一条语句是数组的声明,它和声明一个相应类型的原始数据类型变量十分相似,只有类型名之后的方括号说明我们声明的是一个数组。第二条语句中的关键字new使Java创建了这个数组。我们需要在运行时明确地创建数组的原因是Java编译器在编译时无法知道应该为数组预留多少空间(对于原始类型则可以)。for语句初始化了数组的N个元素,将它们的值置为0.0。在代码中使用数组时,一定要依次声明、创建并初始化数组。忽略了其中的任何一步都是很常见的编程错误。

1.1.4.2 使用数组

1.2 数据抽象

1.2.1 创建对象

每种数据类型中的值都存储于一个对象中。要创建(或实例化)一个对象,我们用关键字new并紧跟类名以及()(或在括号中指定一系列的参数,如果构造函数需要的话)来触发它的构造函数。构造函数没有返回值,因为它总是返回它的数据类型的对象的引用。每当用例调用了new(),系统都会:

  • 为新的对象分配内存空间;
  • 调用构造函数初始化对象中的值;
  • 返回该对象的一个引用。

1.2.2 几何对象

面向对象编程的一个典型例子是为几何对象设计数据类型。例如,表1.2.3至表1.2.5中的API为三种常见的几何对象定义了相应的抽象数据类型:Point2D(平面上的点)、Interval1D(直线上的间隔)、Interval2D(平面上的二维间隔,即和数轴对齐的长方形)。

1.3 包、队列和栈

许多基础数据类型都和对象的集合有关。具体来说,数据类型的值就是一组对象的集合,所有操作都是关于添加、删除或是访问集合中的对象。分别是包(Bag)、队列(Queue)和栈(Stack)。它们的不同之处在于删除或者访问对象的顺序不同。

学习目标

  • 了解集合中的对象的表示方式如何直接影响各种操作的效率。
  • 了解泛型和迭代。
  • 了解链式数据结构-链表。【重点】

表1.3.1 泛型可迭代的基础集合数据类型的API

1.3.1.1 泛型

集合类的抽象数据类型的一个关键特性是我们可以用它们存储任意类型的数据。一种特别的Java机制能够做到这一点,它被称为泛型,也叫做参数化类型。

1.3.1.2 自动装箱

类型参数必须被实例化为引用类型,因此Java有一种特殊机制来使泛型代码能够处理原始数据类型。我们还记得Java的封装类型都是原始数据类型所对应的引用类型:Boolean、Byte、Character、Double、Float、Integer、Long和Short分别对应着boolean、byte、char、double、float、int、long和short。在处理赋值语句、方法的参数和算术或逻辑表达式时,Java会自动在引用类型和对应的原始数据类型之间进行转换。在这里,这种转换有助于我们同时使用泛型和原始数据类型。例如:

Stack <Integer> stack = new Stack<Integer>();
stack.push(17);       //自动装箱(int->Integer)
Int i = stack.pop();  //自动拆箱(Integer->int)

自动将一个原始数据类型转换为一个封装类型被称为自动装箱,自动将一个封装类型转换为一个原始数据类型被称为自动拆箱。在这个例子中,当我们将一个原始类型的值17传递给push()方法时,Java将它的类型自动转换(自动装箱)为Integer。pop()方法返回了一个Integer类型的值,Java在将它赋予变量i之前将它的类型自动转换(自动拆箱)为了int。

1.3.1.3 队列

队列是一种基于先进先出(FIFO)策略的集合类型,如图1.3.2所示。按照任务产生的顺序完成它们的策略我们每天都会遇到:在剧院门前排队的人们、在收费站前排队的汽车或是计算机上某种软件中等待处理的任务。任何服务性策略的基本原则都是公平。 队列是许多日常现象的自然模型,它也是无数应用程序的核心。当用例使用foreach语句迭代访问队列中的元素时,元素的处理顺序就是它们被添加到队列中的顺序。在应用程序中使用队列的主要原因是在用集合保存元素的同时保存它们的相对顺序:使它们入列顺序和出列顺序相同。例如,下页的用例是我们的In类的静态方法readInts()的一种实现。这个方法为用例解决的问题是用例无需预先知道文件的大小即可将文件中的所有整数读入一个数组中。我们首先将所有的整数读入队列中,然后使用Queue的size()方法得到所需数组的大小,创建数组并将队列中的所有整数移动到数组中。队列之所以合适是因为它能够将整数按照文件中的顺序放入数组中(如果该顺序并不重要,也可以使用Bag对象)。这段代码使用了自动装箱和拆箱来转换用例中的int原始数据类型和队列的Integer封装类型。

//Queue的用例
public static int[] readInts(String name){
    In in = new In(name);
    Queue<Integer> q = new Queue<Integer>();
    while(!in.isEmpty())
        q.enqueue(in.readInt());
    int N = q.size();
    int[] a = new int[N];
    for(int i=0;i<N;i++)
        a[i] = q.dequeue();
    return a;
}

1.3.1.4 栈

栈是一种基于后进先出(LIFO)策略的集合类型,如图1.3.3所示。当你的邮件在桌上放成一叠时,使用的就是栈。新邮件来到时你将它们放在最上面,当你有空时你会一封一封地从上到下阅读它们。现在人们应付的纸质品比以前少得多,但计算机上的许多常用程序遵循相同的组织原则。例如,许多人仍然采用栈的方式存放电子邮件——在收信时将邮件压入(push)最顶端,在取信时从最顶端将它们弹出(pop),这种策略的好处是我们能够及时看到感兴趣的邮件,坏处是如果你不把栈清空,某些较早的邮件可能永远也不会被阅读。你在网上冲浪时很可能会遇到栈的另一个例子。点击一个超链接,浏览器会显示一个新的页面(并将它压入一个栈)。你可以不断点击超链接并访问新页面,但总是可以通过点击“回退”按钮重新访问以前的页面(从栈中弹出)。栈的后进先出策略正好能够提供你所需要的行为。当用例使用foreach语句迭代遍历栈中的元素时,元素的处理顺序和它们被压入的顺序正好相反。在应用程序中使用栈迭代器的一个典型原因是在用集合保存元素的同时颠倒它们的相对顺序。例如,右侧的用例Reverse将会把标准输入中的所有整数逆序排列,同样它也无需预先知道整数的多少。在计算机领域,栈具有基础而深远的影响,下一节我们会仔细研究一个例子,以说明栈的重要性。

1.3.2 集合类数据类型的实现

1.3.2.3 调整数组大小

选择用数组表示栈内容意味着用例必须预先估计栈的最大容量。在Java中,数组一旦创建,其大小是无法改变的,因此栈使用的空间只能是这个最大容量的一部分。选择大容量的用例在栈为空或几乎为空时会浪费大量的内存。例如,一个交易系统可能会涉及数十亿笔交易和数千个交易的集合。即使这种系统一般都会限制每笔交易只能出现在一个集合中,但用例必须保证所有集合都有能力保存所有的交易。另一方面,如果集合变得比数组更大那么用例有可能溢出。为此,push()方法需要在代码中检测栈是否已满,我们的API中也应该含有一个isFull()方法来允许用例检测栈是否已满。我们在此省略了它的实现代码,因为我们希望用例从处理栈已满的问题中解脱出来,如我们的原始Stack API所示。因此,我们修改了数组的实现,动态调整数组a[]的大小,使得它既足以保存所有元素,又不至于浪费过多的空间。实际上,完成这些目标非常简单。首先,实现一个方法将栈移动到另一个大小不同的数组中:

private void resize(int max){
      //将大小为N<=max的栈移动到一个新的大小为max的数组中
      Item[] temp = (Item[]) new Object[max]
      for(int i=0;i<N;i++)
         temp[i] = a[i];
         a = temp;  
}

现在,在push()中,检查数组是否太小。具体来说,我们会通过检查栈大小N和数组大小a.length是否相等来检查数组是否能够容纳新的元素。如果没有多余的空间,我们会将数组的长度加倍。然后就可以和从前一样用a[N++] = item插入新元素了:

public void push(Item item){
    //将元素压入栈项
    if(N==a.length) resize(2*a.length);
    a[N++] = item;
}

类似,在pop()中,首先删除栈顶的元素,然后如果数组太大我们就将它的长度减半。只要稍加思考,你就明白正确的检测条件是栈大小是否小于数组的四分之一。在数组长度被减半之后,它的状态约为半满,在下次需要改变数组大小之前仍然能够进行多次push()和pop()操作。

public Item pop(){
    //从栈顶删除元素
    Item item = a[--N];
    a[N] = null; //避免对象游离
    if(N>0&&N==a.length/4)resize(a.length/2);
    return item;
}

在这个实现中,栈永远不会溢出,使用率也永远不会低于四分之一(除非栈为空,那种情况下数组的大小为1)。我们会在1.4节中详细分析这种实现方法的性能特点。

1.3.2.4 对象游离

Java的垃圾收集策略是回收所有无法被访问的对象的内存。在我们对pop()的实现中,被弹出的元素的引用仍然存在于数组中。这个元素实际上已经是一个孤儿了——它永远也不会再被访问了,但Java的垃圾收集器没法知道这一点,除非该引用被覆盖。即使用例已经不再需要这个元素了,数组中的引用仍然可以让它继续存在。这种情况(保存一个不需要的对应的引用)称为游离。 在这里,避免对象游离很容易,只需将被弹出的数组元素的值设为null即可,这将覆盖无用的引用并使系统可以在用例使用完被弹出的元素后回收它的内存。

1.3.2.5 迭代

这里,foreach语句只是while语句的一种简写方式(就好像for语句一样)。它本质上和以下while语句是等价的:

Iterator<String> i = collection.iterator();
while(i.hasNnet()){
    String s = i.next();
    StdOut.println(s);
}

这段代码展示了一些在任意可迭代的集合数据类型中我们都需要实现的东西:

  • 集合数据类型必须实现一个iterator()方法并返回一个Iterator对象;
  • Iterator类必须包含两个方法:hasNext(返回一个布尔值)和next()(返回集合中的一个泛型元素)。

1.3.3 链表

链表是一种递归的数据结构,它或者为空(null),或者是指向一个结点(node)的引用,该结点含有一个泛型的元素和一个指向另一条链表的引用。

在这个定义中,结点是一个可能含有任意类型数据的抽象实体,它所包含的指向结点的应用显示了它在构造链表之中的作用。

1.3.3.1 结点记录

结点: 一般结点是有存储功能的,比较独立的存在。

节点: 是一连串时间的阶段性标志。

在面向对象编程中,实现链表并不困难。我们首先用一个嵌套类来定义结点的抽象数据类型:

private class Node{
    Item item;
    Node next;
}

1.3.3.2 构造链表

现在,根据递归定义,我们只需要一个Node类型的变量就能表示一条链表,只要保证它的值是null或者指向另一个Node对象且该对象的next域指向了另一条链表即可。例如,要构造一条含有元素to、be和or的链表,我们首先为每个元素创造一个结点:

Node first = new Node();
Node second = new Node();
Node third = new Node();		

并将每个结点的item域设为所需的值(简单起见,我们假设在这些例子中Item为String):

first.item = "to";
second.item = "be";
third.item = "or";

然后设置next域来构造链表:

first.next = second;
second.next = third;

(注意:third.next仍然是null,即对象创建时它被初始化的值。)

它们都是链表

third:是一个结点的引用,该结点指向null,即一个空链表;

second:是一个结点的引用,且该结点含有一个指向third的引用,而third是一条链表;

first:是一个结点的引用,且该结点含有一个指向second的引用,而second是一条链表

链表表示的是一列元素。 在我们刚刚考察过的例子中,first表示的序列是to、be、or。我们也可以用一个数组来表示一列元素。例如,可以用以下数组表示同一列字符串

String[] s = {"to","be","or"};

不同之处在于,在链表中向序列插入元素或是从序列中删除元素都更方便。

1.3.3.3 栈的实现

链表的使用达到了我们的最优设计目标:

  • 它可以处理任意类型的数据;
  • 所需的空间总是和集合的大小成正比;
  • 操作所需的时间总是和集合的大小无关。

Stack的测试用例:

public static void main(String[] args){
    //创建一个栈并根据StdIn中的指示压入或弹出字符串
    Stack<String> s = new Stack<String>();
    while(!StdIn.isEmpty()){
        String item = StdIn.readString();
        if(!item.equals(“-”)){
            s.push(item);
        }else if(!s.isEmpty()){
            StdOut.print(s.pop()+" ")
        }
        StdOut.println("("+s.size()+" left on stack)")
    }
}

这份实现是我们对需对算法实现的原型。它定义了链表数据结构并实现了供用例使用的方法push()和pop(),仅用了少量代码就取得了所期望的效果。

算法1.2下压堆栈(链表实现)

public class Stack<Item> implements Iterable<Item>{
    private Node first;//栈顶(最近添加的元素)
    private int N;//元素数量
    private class Node{//定义了结点的嵌套类
        Item item;
        Node next;
    }
    public void push(Item item){//向栈顶添加元素
        Node oldfirst = first;
        first = new Node();
        first.item = item;
        first.next = oldfirst;
        N++;
    }
    public Item pop(){//从栈顶删除元素
       Item item = first.item;
        first = first.next;
        N--;
        return item;
    }
}
% more tobe.txt
to be or not to - be - - that - - - is
% java Stack < tobe.txt
to be not that or be (2 left no stack) 

1.3.3.4 队列的实现

基于链表数据结构实现 Queue API也很简单,如算法1.3所示。它将队列表示为一条从早插入的元素到最近插入的元素的链表,实例变量first指向队列的开头,实例变量last执行队列的结尾。这样,要将一个元素入列(enqueue()),我们就将它添加到表尾(请见图1.3.8中讨论的代码,但是在链表为空时需要将first和last都指向新结点);要将一个元素出列(dequeue()),我们就删除表头的结点(代码和Stack的pop()方法相同,只是当链表为空时需要更新last的值)。size()和isEmpty()方法的实现和Stack相同。和Stack一样,Queue的实现也使用了泛型参数Item。这里我们省略了支持迭代的代码并将它们留到算法1.4中继续讨论。下面所示的是一个开发用例,它和我们在Stack中使用的用例很相似,它的轨迹如算法1.3所示。Queue的实现使用的数据结构和Stack相同——链表,但它实现了不同的添加和删除元素的算法,这也是用例所看到的后进先出和先进后出的区别所在。和刚才一样,我们用链表达到了最优设计目标:它可以处理任意类型的数据,所需的空间总是和集合的大小成正比,操作所需的时间总是和集合的大小无关。

public static void mian(String[] args){
    //创建一个队列并操作字符串入列或出列
    Queue<String> q = new Queue<String>();
    while(!StdIn.isEmpty()){
        String item =  StdIn.readString();
        if(!item.equals("-")){
            q.enqueue(item);
        }else if(!q.isEmpty()){
            StdOut.print(q.dequeue()+" ");
        }
        StdOut.println("(" + q.size() + " left on queue)");
    }
}
% more tobe.txt
to be or not to - be - - that - - - is
% java Stack < tobe.txt
to be not that or be (2 left on queue)

算法1.3先进先出队列

public class Queue<Item> implements Iterable<Item>{
    private Node first;// 指向最早添加的结点的链接
    private Node last;// 指向最近添加的结点的链接
    private int N;// 队列的元素数量
    private class Node{// 定义了结点的嵌套类
        Item item;
        Node next;
    }
    public boolean isEmpty(){
        return first == null; //或:N==0 
    }
    public int size(){
        return N;
    }
    public void enqueue(Item item){//向表尾添加元素
        Node oldlast = last;
        last = new Node();
        last.item = item;
        last.next = null;
        if(isEmpty()){
            first = last;
        }else{
            oldlast.next = last;
            N++;
        }
        public Item dequeue(){
            // 从表头删除元素
            Item item = first.item;
            first = first.next;
            if(isEmpty()){
                last = null;
                N--;
                return item;
            }
        }    
    }
}

这份泛型的Queue实现的基础是链表数据结构。它可以用于创建任意数据类型的队列。

Queue的开发用例的轨迹:

在结构化存储数据集时,链表是数组的一种替代方式

算法1.4背包

import java.util.Iterator;

public class Bag<Item> implements Iterable<Item>{
    private Node first; //链表的首结点
    private class Node{
         Item item;
         Node next;
    }
    public void add(Item item){//和Stack的push()方法完全相同
        Node oldfirst = first;
        first = new Node();
        first.item = item;
        first.next = oldfirst;
    }
    public Iterator<Item> iterator(){
        return new ListIterator();
    }
    private class ListIterator implements Iterator<Item>{
        private Node current = first;
        public boolean hasNext(){
            return current!=null;
        }
        public void remove(){}
        public Item next(){
            Item item = current.item;
            current = current.next;
            return item;
        }
    }
}

这份Bag的实现维护了一条链表,用于保存所有通过add()添加的元素。size()和isEmpty()方法的代码和Stack中的完全相同,因此在此处省略。迭代器会遍历链表并将当前结点保存在current变量中。我们可以将加粗的代码添加到算法1.2和算法1.3中使Stack和Queue变为可迭代的,因为它们背后的数据结构是相同的,只是Stack和Queue的链表访问顺序分别是后进先出和先进先出而已。

数据结构

数据结构优点缺点
数组通过索引可以直接访问任意元素在初始化时就需要知道元素的数量
链表使用的空间大小和元素数量成正比需要通过引用访问任意元素
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值