线性表
线性表是最基本、最简单、也是最常用的一种数据结构。一个线性表是$n$个具有相同特性的数据元素的有限序列。
前驱元素:若A元素在B元素的前面,则称A为B的前驱元素。
后继元素:若B元素在A元素的后面,则称B为A的后继元素。
线性表的特征:数据元素之间具有一种“一对一”的逻辑关系。
- 第一个数据元素没有前驱,这个元素被称为头结点;
- 最后一个数据元素没有后继,这个元素被称为尾结点;
- 除了第一个和最后一个数据元素外,其他数据元素有且仅有一个前驱和一个后继。
如果把线性表用数学语言来定义,则可以表示为$(a_1,a_2…a_i,a_{i+1}…a_n)$,称$a_{i-1}$是$a_i$的前驱元素,$a_{i+1}$是$a_i$的后继元素。
线性表的分类:
线性表中数据存储的方式可以是顺序存储,也可以是链式存储,按照数据的存储方式不同,可以把线性表分为顺序表和链表。
顺序表
顺序表是在计算机内存中以数组的形式保存的线性表,线性表的顺序存储是指一组地址连续的存储单元,依次存储线性表中的各个元素、使得线性表中在逻辑结构上相应的数据元素存储在相邻的物理存储单元中,即通过数据元素物理存储的相邻关系来反映数据元素之间逻辑上的相邻关系。
顺序表的API设计:
类名 | SequenceList\ |
---|---|
构造方法 | SequenceList(int capacity) |
成员方法 | public void clear() |
public boolean isEmpty() | |
public int length() | |
public T get(int i) | |
public void insert(int i, T t) | |
public void insert(T t) | |
public T remove(int i) | |
public int indexOf(T t) | |
成员变量 | private T[] eles |
private int N |
1 | public class SequenceList<T>{ |
1 | public class SequenceListTest{ |
顺序表的遍历:
一般作为容器存储数据,都需要向外部提供遍历的方式,因此我们需要给顺序表提供遍历方式。在Java中,遍历集合的方式一般都是$foreach$循环,如果想让我们的$SequenceList$也能支持$foreach$循环,则需要做如下操作:
- 让$SequenceList$实现$Iterable$接口,重写iterator方法;
- 让$SequenceList$内部提供一个内部类$SIterator$,实现$Iterator$接口,重写$hasNext$方法和$next$方法;
1 | public class SequenceList<T> implements Iterable<T>{ |
顺序表的容量可变:
在之前的实现中,当我们使用$SequenceList$时,先$new SequenceLsit(5)$创建一个对象,创建对象时就需要指定容器大小,初始化指定大小的数组来存储元素,当我们插入元素时,如果已经插入了5个元素,还要继续插入数据,就会报错,就不能插入了。这种设计不符合容器的设计理念,因此我们在设计顺序表时,应该它的容量的伸缩性。
- 添加元素时:添加元素时,应该检查当前数组的大小是否能够容纳新的元素,如果不能容纳,则需要创建新的容量更大的数组,我们这里创建一个是原数组两倍容量的新数组存储元素。
- 移除元素时,应该检查当前数组的大小是否太大,比如正在用100个容量的数组存储10个元素,这样就会造成内存空间的浪费,应该创建一个容器更小的数组存储元素。如果我们发现数据元素的数量不足数组容量的1/4,则创建一个是原数组容量的1/2的新数组存储元素。
1 | public class SequenceList<T> implements Iterable<T>{ |
顺序表的时间复杂度:
- $get(i)$:不难看出,不论数据元素量N有多大,只需要$eles[i]$就可以获取到对应的元素,所以时间复杂度为$O(1)$;
- $insert(int i, T t)$:每一次插入,都需要把$i$位置后面的元素移动一次,随着元素数量$N$的增大,移动的元素也越多,时间复杂度为$O(n)$;
- $remove(int i)$:每一次删除,都需要把$i$位置后面的元素移动一次,随着元素数量$N$的增大,移动的元素也越多,时间复杂度为$O(n)$;
由于顺序表的底层由数组实现,数组的长度是固定的,所以在操作的过程中涉及到了容器扩容操作。这样会导致顺序表在使用过程中的时间复杂度不是线性的,在某些需要扩容处的节点处,耗时会突增,尤其是元素越多,这个问题越明显。
Java中$ArrayList$实现:
Java中$ArrayList$集合的底层也是一种顺序表,使用数组实现,同样提供了增删改查以及扩容等功能。
链表
前面已经使用顺序存储结构实现了线性表,我们可以发现虽然顺序表的查询很快,时间复杂度为$O(1)$,但是增删的效率是比较低的,因为每一次增删操作都伴随着大量的数据元素移动。
链表是一种物理存储单元非连续、非顺序的存储结构,其物理结构不能只管的表示数据元素的逻辑顺序,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列的结点(链表中的每一个元素称为结点)组成,结点可以在运行时动态生成。
结点API设计:
类名 | Node\ |
---|---|
构造方法 | Node(T t, Node next) |
成员变量 | T item |
Node next |
结点类的实现:
1 | public class Node<T>{ |
生成链表:
1 | public static void main(String[] args) throws Exception{ |
单向链表
单向链表是链表的一种,它由多个结点组成,每个结点都由一个数据域和一个指针域组成,数据域用来存储数据,指针域用来指向其后继结点。链表的头结点的数据域不存储数据,指针域指向第一个真正存储数据的结点。
单向链表的API设计:
类名 | LinkList\ |
---|---|
构造方法 | LinkList() |
成员方法 | public void clear() |
public boolean isEmpty() | |
public int length() | |
public T get(int i) | |
public void insert(T t) | |
public void insert(int i, T t) | |
public T remove(int i) | |
public int indexOf(T t) | |
成员内部类 | private class Node\ |
成员变量 | private Node head |
private int N |
单向链表的代码实现:
1 | public class LinkList<T> implements Iterable<T>{ |
双向链表
双向链表也叫双向表,是链表的一种,它由多个结点组成,每个结点都由一个数据域合和两个指针域组成,数据域用来存储数据,其中一个指针域用来指向后继结点,另一个指针用来指向前驱结点。链表的头结点的数据域不存储数据,指向前驱结点的指针值为null,指向后继结点的指针域指向第一个真正存储数据的结点。
结点API的设计:
类名 | Node\ |
---|---|
构造方法 | Node(T t, Node pre, Node next) |
成员变量 | T item |
Node next | |
Node pre |
双向链表API的设计:
类名 | TowWayLinkList\ |
---|---|
构造方法 | TowWayLinkList() |
成员方法 | public void clear() |
public boolean isEmpty() | |
public int length() | |
public void insert(T t) | |
public void insert(int i, T t) | |
public T remove(int i) | |
public int indexOf(T t) | |
public T getFirst() | |
public T getLast() | |
成员内部类 | private class Node\ |
成员变量 | private Node first |
private Node last | |
private int N |
双向链表代码实现:
1 | public class TowWayLinkList<T> implements Iterable<T>{ |
Java中LinkedList实现:
java中LinkedList集合也是使用双向链表实现,并提供了增删改查等相关方法。
链表的复杂度分析:
- $get(int i)$:每一次查询,都需要从链表的头部开始,依次向后查找,随着数据元素N的增多,比较元素越多,时间复杂度为$O(n)$。
- $insert(int i, T t)$:每一插入,需要找到i位置的前一个元素,然后完成插入操作,随着元素数量N的增多,查找的元素越多,时间复杂度为$O(n)$。
- $remove(int i)$:每一次移除,需要先找到i位置的前一个元素,然后完成插入操作,随着数据元素N的增多,查找的元素越多,时间复杂度为$O(n)$。
相比较顺序表,链表插入和删除的时间复杂度虽然一样,然仍然有很大的优势,因为链表的物理地址是不连续的,它不需要预定指定存储空间大小,或者在存储过程中涉及到扩容等操作,同时它并没有涉及到元素的交换。
相比较顺序表,链表的查询操作性能会比较低。因此,如果我们的程序中查询操作比较多,建议使用顺序表,增删操作比较多,建议使用链表。
链表反转:
需求:
- 原链表中数据为:1->2->3->4
- 反转后链表中数据为:4->3->2->1
反转API:
- public void reverse():对整个链表反转
- public Node reverse(Node curr):反转链表中某个节点curr,并把反转后的curr结点返回
使用递归可以完成反转,递归反转其实就是从原链表的第一个存放数据的结点开始,依次递归调用反转每一个节点,直到把最后一个结点反转完毕,整个链表就反转完毕。
1 | public void reverse(){ |
快慢指针:
快慢指针的定义是,这两个指针的移动速度一快一慢,以此来制造出自己想要的差值,这个差值可以让我们找到链表上相应的结点。一般情况下,快指针的移动步长为慢指针的两倍。
中间值问题:
1 | public static String getMid(Node<String> first){ |
单向链表是否有环问题:
1 | public static boolean isCircle(Node first){ |
有环链表入口问题:
1 | public static Node getEntrance(Node first){ |
循环链表:
链表整体要形成一个圆环状。在单向链表中,最后一个结点的指针为null,不指向任何结点,因为没有下一个元素了。要实现循环链表,我们只需要让单向链表的最后一个结点的指针指向头结点即可。
约瑟夫问题:
41个人坐一圈,第一个人编号为1,第二个编号为2,第n个人编号为n。编号为1的人开始报数,依次向后,报数为3的那个人退出圈;自退出那个人开始的下一个人再次从1开始报数,以此类推;求出最后退出的那个人的编号。
解题思路:
构建含有41个结点的单向循环链表,分别存储1~41的值,代表这41个人;使用计数器count,记录当前报数的值;遍历链表,每循环一次,count++;判断count的值,如果是3,则从链表中删除这个结点并打印结点的值,把count重置为0。
代码实现:
1 | public class JosephTest{ |
栈
栈是一种基于先进后出(FILO)的数据结构,是一种只能进行插入和删除操作的特殊线性表。它按照先进后出的原则存储数据,先进入的数据被压入栈底,最后的数据在栈顶,需要读数据的时候从栈顶开始弹出数据(最后一个数据被第一个读出来)。
我们称数据进入到栈的动作为压栈,数据从栈中出去的动作为弹栈。
栈的API设计:
类名 | Stack\ |
---|---|
构造方法 | Stack() |
成员方法 | public boolean isEmpty() |
public int size() | |
public T pop() | |
public void push(T t) | |
成员变量 | private Node head |
private int N | |
成员内部类 | private class Node |
栈的代码实现:
1 | public class Stack<T> implements Iterable<T>{ |
括号匹配问题:
问题描述:
给定一个字符串,里面可能包含“()”小括号和其他字符,通过程序检查该字符串中的小括号是否成对出现。
例如:
- “(上海)(长安)”正确匹配
- “(上海((长安))”正确匹配
- “上海(长安(北京)(深圳)南京)”正确匹配
- “上海(长安))”匹配错误
- “((上海)长安”匹配错误
示例代码:
1 | public class BracketsMatchTest{ |
逆波兰表达式求值问题
中缀表达式:
中缀表达式就是生活中使用的表达式,例如:$1+3*2,2-(1+3)$等等,中缀表达式的特点是:二元运算符总是置于两个操作数中间。中缀表达式是人们最喜欢的表达方式,因为简单。但对于计算机来说就不是这样了,因为中缀表达式的运算顺序不具有规律性。不同的运算符具有不同的优先级,如果计算机执行中缀表达式,需要解析表达式语义,做大量的优先级相关操作。
逆波兰表达式(后缀表达式):
逆波兰表达式是波兰逻辑学家J·卢卡西维兹于1929年提出的一种表达式的表示方法,后缀表达式的特点:运算符总是放在跟它相关的操作数之后。
中缀表达式 | 逆波兰表达式 |
---|---|
a+b | ab+ |
a+(b-c) | abc-+ |
a+(b-c)*d | abc-d*+ |
a*(b-c)+d | abc-*d+ |
需求:
给定一个只包含加减乘除四种运算的逆波兰表达式的数组表示方法,求出该逆波兰表达式的结果。
1 | public class ReversePolishNotationTest{ |
分析:
- 创建一个栈对象oprands存储操作数
- 从左向右依次遍历逆波兰表达式,拿到每一个字符
- 判断字符串是否为运算符?如果不是,把该操作数压入栈中
- 如果是运算符,则从栈中弹出两个操作数
- 使用该运算符计算得到结果
- 把该结果压入栈中
- 遍历结束后,拿出栈中的结果返回
队列
队列是一种基于先进先出(FIFO)的数据结构,是一种只能在一端进行插入,在另一端进行删除操作的特殊线性表,它按照先进先出的原则存储数据,先进入的数据,在读取数据时先被读出来。
队列的API设计:
类名 | Queue |
---|---|
构造方法 | Queue() |
成员方法 | public boolean isEmpty() |
public int size() | |
public T dequeue() | |
public void enqueue(T t) | |
成员变量 | private Node head |
private int N | |
private Node last | |
成员内部类 | private class Node |
队列的代码实现:
1 | public class Queue<T> implements Iterable<T>{ |