第三章 数据结构
目录
3.1 线性表
线性表是最基本、最常见的一种数据结结构,线性表是n个具有相同特征的数据元素的有限序列
常见场景排队 :
前驱元素
若A元素在B元素的前面,则称A为B的前驱元素
后继元素
若B元素在A元素的后面,则称A为B的后继元素
线性表特性 : 数据元素之间具有1对1的逻辑关系
- 第一个元素没有前驱元素,称为头节点
- 最后一个元素没有后继元素, 称为尾节点
- 除了第一个元素和最后一个元素,其他元素有且仅有一个前驱和一个后继
线性表用数学语言来定: a1 ,a2 , ..., ai-1 , ai,ai+1,..., an
线性表的分类 :
顺序存储结构 : 顺序表
链式存储结构 : 链表
顺序表存储数据时,会提前申请一整块足够大小的物理空间,然后将数据依次存储起来,存储时做到数据元素之间不留一丝缝隙。
链表不限制数据的物理存储状态,换句话说,使用链表存储的数据元素,其物理存储位置是随机的。
3.1.1 顺序表
顺序表的存储元素之间地址是连续的,数组刚好是这种存储结构, 所以使用数组实现顺序表
顺序表存储数据之前,除了要申请足够大小的物理空间之外,为了方便后期使用表中的数据,顺序表还需要实时记录以下 2 项数据:
- 顺序表申请的存储容量;
- 顺序表的长度,也就是表中存储数据元素的个数;
顺序表的API设计
类名 | SequenceList<T> |
构造方法 | SequenceList(int capacity) : 创建容量为capacity的SequeceList对象 |
成员方法 |
|
成员变量 |
|
代码实现 :
/**
* 数组实现 顺序表
* 实现Iterable可以通过这种方式迭代 for(ele:eles)
*
* 支持扩容: 增加元素-扩容, 删除元素-缩容
* 扩容,增加数据之前判断,容量不够,创建新数组是原来数组的2倍,同时copy原来数组数据
* 缩容,移除数据之后,如果长度<容量的1/4,就创建容量的一半缩容数组,将原来的数据copy过来
*
* 时间复杂度
* get方法指定索引,复杂度O(1)
* insert 插入,如果插入是第一个元素,就要全部移动,复杂度是O(n)
* remove 删除复杂都也是O(n)
* 扩缩容 : 如果插入某个元素的时候触发了扩缩容,这个时候插入数据前需要申请内存,消耗的时间就不是线性的了,会突然消耗很多时间;
* @param <T>
*/
public class SequenceList<T> implements Iterable<T>{
private T[] eles;
private int N;
public SequenceList(int capacity) {
this.eles = (T[]) new Object[capacity];
this.N = 0;
}
/**
* 清空线性表
*/
public void clear() {
this.N = 0;
}
/**
* 判断线性表是否为空
* @return
*/
public boolean isEmpty() {
return this.N == 0;
}
/**
* 返回线性表长度
* @return
*/
public int length() {
return this.N;
}
/**
* 返回第i个元素
* @param i
* @return
*/
public T get(int i) {
if (i>N || i<0 ) return null;
return eles[i];
}
/**
* 插入一个元素,默认是线性表的最后面
* @param t
*/
public void insert(T t) {
if (N == eles.length) {
resize(2* eles.length);
}
eles[N++] = t;
}
/**
* 第i个元素前插入,就是ele[i]
* @param i
* @param t
*/
public void insert(int i, T t) {
if (N == eles.length) {
resize(2* eles.length);
}
//i处元素和后面元素都向后移
for (int index = N; index >i; index--) {
eles[index]=eles[index-1];
}
//赋值
eles[i] =t;
N++;
}
/***
* 删除元素,
* @param i
* @return
*/
public T remove(int i) {
T t = eles[i];
for (int index = i; index < N ; index++) {
eles[index] = eles[index+1];
}
N--;
if (N < eles.length/4){
resize(eles.length/2);
}
return t;
}
/**
* 返回线性表中首次出现t元素的位置索引,若不存在返回-1
* @param t
* @return
*/
public int indexOf(T t) {
for (int i = 0; i < eles.length; i++) {
t.equals(eles[i]);
return i;
}
return -1;
}
/**
* 根据newsize重新更新数组
* @param newSize
*/
public void resize(int newSize) {
//定义临时数组,指向原数组
T[] temps = eles;
//创建新数组
eles = (T[]) new Object[newSize];
//把原来数组的输copy到新数组
for (int i = 0; i < N; i++) {
eles[i] = temps[i];
}
}
/**
* 迭代返回
* @return
*/
public Iterator<T> iterator() {
return new SIterator();
}
/**
* 内部类实现
*
*/
private class SIterator implements Iterator {
private int cursor;
public SIterator() {
this.cursor = 0;
}
public boolean hasNext() {
return cursor < N;
}
public T next() {
return eles[cursor++];
}
public void remove() {
}
}
}
Java的ArrayList 就是顺序表的实现,底层也是通过数组实现的而且提供了自动扩容方法;
- 为什么自己要实现?
Java本身的顺序表的实现是考虑各种情况, 代码也是比较多,比较通用, 但是也比较臃肿, 在实际过程中,有可能ArrayList不能 ,可以通过自己的实现的顺序表,帮助解决特殊问题。自己写的代码不一定就比ArrayList差。
3.1.2 链表
链表中每个数据的存储都由以下两部分组成:
- 数据元素本身,其所在的区域称为数据域;
- 指向直接后继元素的指针,所在的区域称为指针域;
链表的存储结构在链表中称为节点。也就是说,链表实际存储的是一个一个的节点,真正的数据元素包含在这些节点中,如图 4 所示:
头节点,头指针和首元节点
其实,图 4 所示的链表结构并不完整。一个完整的链表需要由以下几部分构成:
- 头指针:一个普通的指针,它的特点是永远指向链表第一个节点的位置。很明显,头指针用于指明链表的位置,便于后期找到链表并使用表中的数据;
- 节点:链表中的节点又细分为头节点、首元节点和其他节点:
- 头节点:其实就是一个不存任何数据的空节点,通常作为链表的第一个节点。对于链表来说,头节点不是必须的,它的作用只是为了方便解决某些实际问题;
- 首元节点:由于头节点(也就是空节点)的缘故,链表中称第一个存有数据的节点为首元节点。首元节点只是对链表中第一个存有数据节点的一个称谓,没有实际意义;
- 其他节点:链表中其他的节点;
因此,一个存储 {1,2,3}
的完整链表结构如图 5 所示:
图 5 完整的链表示意图
注意:链表中有头节点时,头指针指向头节点;反之,若链表中没有头节点,则头指针指向首元节点:
链表的插入
链表的删除:
结点的类设计
类名 | Node<T> 结点类 |
构造方法 | Node(T t,Node next) : 创建node对象 |
成员方法 | T item 存储的数据 Node next ; 指向下一个结点 |
代码实现
public class Node<T> {
public T t;
public Node next;
public Node(T t, Node next) {
this.t = t;
this.next = next;
}
}
简单测试 : 构建简单链表
public class TestNode {
public static void main(String[] args) {
//构建结点
Node<Integer> first = new Node<Integer>(12,null);
Node<Integer> second = new Node<Integer>(13,null);
Node<Integer> third = new Node<Integer>(14,null);
Node<Integer> fourth = new Node<Integer>(15,null);
Node<Integer> fifth = new Node<Integer>(15,null);
//生成链表
first.next =second;
second.next = third;
third.next = fourth;
fourth.next = fifth;
}
}
单向链表
单向链表的API
类名 | LinkList<T> |
构造方法 | LinkList() : 创建LinkList对象 |
成员方法 |
|
成员变量 |
|
成员内部类 | private class Node<T> 结点类 |
代码实现:
/**
* 时间复杂度
* get 方法指定索引,复杂度O(n)
* insert 插入,如果插入是第一个元素,就要全部移动,复杂度是O(n)
* remove 删除复杂都也是O(n)
*/
public class LinkList<T> implements Iterable<T> {
private Node head;
private int N;
public LinkList() {
this.head = new Node(null,null);
this.N = 0;
}
public void clear() {
this.head.next = null;
this.N = 0 ;
}
public int length() {
return this.N;
}
public boolean isEmpty() {
return this.N == 0 ;
}
/**
* 获取指定位置i的元素
* @param i
* @return
*/
public T get(int i) {
Node node = head.next;
for (int j = 0; j < i; j++) {
node = node.next;
}
return node.t;
}
/**
* 添加一个元素,默认链表最后
* @param t
*/
public void insert(T t) {
Node newNode = new Node(t, null);
Node node = head;
while (node.next != null) {
node = node.next;
}
node.next = newNode;
N++;
}
/**
* 指定位置,添加一个元素
* @param t
*/
public void insert(int i,T t) {
//前面的元素
Node prior = head;
for (int j = 0; j < i ; j++) {
prior = prior.next;
}
//后面的元素
Node next = prior.next;
//建立联系
Node newNode = new Node(t,next);
prior.next = newNode;
N++;
}
/**
* 删除i元素
* @param i
* @return
*/
public T remove(int i) {
//前面的元素
Node prior = head;
for (int j = 0; j < i ; j++) {
prior = prior.next;
}
//要删除的元素
Node current = prior.next;
prior.next = current.next;
N--;
return current.t;
}
/**
* 返回T的位置i
* @param t
* @return
*/
public int indexOf(T t) {
Node node = head.next;
for (int i = 0; node.next != null ; i++) {
if (t.equals(node.t)) return i;
node = node.next;
}
return -1;
}
private class Node {
public T t;
public Node next;
public Node(T t, Node next) {
this.t = t;
this.next = next;
}
}
public Iterator<T> iterator() {
return new LinkIterator();
}
private class LinkIterator implements Iterator {
private Node node;
public LinkIterator() {
this.node = head;
}
public boolean hasNext() {
return node.next != null ;
}
public T next() {
node = node.next;
return node.t;
}
public void remove() {
}
}
}
测试代码
public class TestLinkList {
public static void main(String[] args) {
LinkList<Integer> integerLinkList = new LinkList<Integer>();
integerLinkList.insert(100);
integerLinkList.insert(200);
integerLinkList.insert(300);
for (int i = 0; i < integerLinkList.length(); i++) {
System.out.println(integerLinkList.get(i));
}
integerLinkList.insert(1,250);
System.out.println("---------insert-------");
for (int i = 0; i < integerLinkList.length(); i++) {
System.out.println(integerLinkList.get(i));
}
integerLinkList.remove(2);
System.out.println("--------remove--------");
for (int i = 0; i < integerLinkList.length(); i++) {
System.out.println(integerLinkList.get(i));
}
System.out.println(integerLinkList.indexOf(100));
System.out.println("--------遍历--------");
for (Integer integer : integerLinkList) {
System.out.println(integer);
}
}
}
双向链表
单链表能 100% 解决逻辑关系为 "一对一" 数据的存储问题,但在解决某些特殊问题时,单链表并不是效率最优的存储结构。比如说,某场景中需要大量地查找某结点的前趋结点,这种情况下使用单链表无疑是灾难性的,因为单链表更适合 "从前往后" 找,"从后往前" 找并不是它的强项。
对于逆向查找(从后往前)相关的问题,双向链表,会更加事半功倍。
双向链表结构示意图:
双向链表结点结构
结点类设计
类名 | Node<T> 结点类 |
构造方法 | Node(T t,Nnode prior, Node next) : 创建node对象 |
成员方法 | T item 存储的数据 Node prior : 指向上一个结点 Node next ; 指向下一个结点 |
双向链表的API设计 :
TwoWayLinkList
类名 | TwoWayLinkList<T> |
构造方法 | TwoWayLinkList() : 创建LinkList对象 |
成员方法 |
|
成员变量 |
|
成员内部类 | private class Node<T> 结点类 |
代码实现 :
public class TwoWayLinkList<T> implements Iterable<T> {
private Node head;
private Node last;
private int N;
public TwoWayLinkList() {
this.head = new Node(null,null,null);
this.last = null;
this.N = 0;
}
/**
* 清空链表
*/
public void clear() {
this.head.next = null;
this.last.prior = null;
this.N = 0 ;
}
public int length() {
return this.N;
}
public boolean isEmpty() {
return this.N == 0 ;
}
/**
* 获取指定位置i的元素
* @param i
* @return
*/
public T get(int i) {
Node node = head;
for (int j = 0; j < i; j++) {
node=node.next;
}
return node.next.t;
}
/**
* 获取第一个元素
* @return
*/
public T getFirst() {
if (isEmpty()) return null;
return head.next.t;
}
/**
* 获取最后一个元素
* @return
*/
public T getLast() {
if (isEmpty()) return null;
return last.t;
}
/**
* 添加一个元素,默认链表最后
* @param t
*/
public void insert(T t) {
Node newNode;
if(isEmpty()) {
newNode = new Node(t, head, null);
head.next = newNode;
} else {
newNode = new Node(t, last, null);
last.next = newNode;
}
last = newNode;
N++;
}
/**
* 指定位置添加一个元素
* @param t
*/
public void insert(int i,T t) {
if (i>length() || i< 0) {
return;
}
//找到上一个结点
Node prior = head;
for (int j = 0; j < i; j++) {
prior = prior.next;
}
//下一个结点
Node next = prior.next;
//当前结点
Node current = new Node(t,prior,next);
//更新上一个结点的next
prior.next = current;
//更新下一个结点的prior
next.prior = current;
N++;
}
/**
* 删除指定位置的结点,返回删除的结点
* @param i
* @return
*/
public T remove(int i) {
//找到上一个结点
Node prior = head;
for (int j = 0; j < i; j++) {
prior = prior.next;
}
//要删除的结点
Node current= prior.next;
//要删除结点的下一个结点
Node next = current.next;
//要删除的上一个结点的next指向next
prior.next = next;
//要删除的下一个结点的prior指向prior
next.prior = prior;
N--;
return current.t;
}
/**
* 返回T的位置i
* @param t
* @return
*/
public int indexOf(T t) {
Node node = head.next;
for (int i = 0; node.next != null; i++) {
if (t.equals(node.t)) return i;
node = node.next;
}
return -1;
}
private class Node {
public T t;
public Node prior;
public Node next;
public Node(T t, Node prior, Node next) {
this.t = t;
this.prior = prior;
this.next = next;
}
}
/**
* 提供遍历
* @return
*/
public Iterator<T> iterator() {
return new TwoLinkIterator();
}
public class TwoLinkIterator implements Iterator{
private Node node;
public TwoLinkIterator() {
this.node = head;
}
public boolean hasNext() {
return node.next !=null;
}
public T next() {
node = node.next;
return node.t;
}
public void remove() {
}
}
}
测试代码:
public class TestTwoLinkList {
public static void main(String[] args) {
TwoWayLinkList<Integer> twoLinkList = new TwoWayLinkList<Integer>();
twoLinkList.insert(100);
twoLinkList.insert(200);
twoLinkList.insert(300);
for (int i = 0; i < twoLinkList.length(); i++) {
System.out.println(twoLinkList.get(i));
}
twoLinkList.insert(0,250);
System.out.println("---------insert-------");
for (int i = 0; i < twoLinkList.length(); i++) {
System.out.println(twoLinkList.get(i));
}
Integer remove = twoLinkList.remove(1);
System.out.println("---------remove-------" + remove);
for (int i = 0; i < twoLinkList.length(); i++) {
System.out.println(twoLinkList.get(i));
}
System.out.println("---------indexOf-------");
System.out.println(twoLinkList.indexOf(200));
System.out.println(twoLinkList.indexOf(100));
System.out.println("--------遍历--------");
for (Integer integer : twoLinkList) {
System.out.println(integer);
}
System.out.println("--------first last--------");
System.out.println(twoLinkList.getFirst());
System.out.println(twoLinkList.getLast());
}
}
3.1.3 顺序表与链表比较
时间复杂度比较 :
数据结构 | insert时间复杂度 | get时间复杂度 | remove时间复杂度 |
顺序表 | O(n) | O(1) | O(n) |
链表 | O(n) | O(n) | O(n) |
结论: 顺序表获取元素是最快,那是因为顺序表的存储地址是连续的,通过索引一次可以获取到需要的元素。如果数据查询比较多,使用顺序表比较好。相对而言,顺序表的插入和删除性能不好。而链表的插入和删除性能更好;
我们都应该听过说,链表的插入和删除是比顺序表性能好,但是他们的时间复杂度都是O(n) ,为什么说链表更好 ?
原因 :链表指定位置,插入也要一个一个开始找位置,n个元素插入,最坏情况每次插入最后一个,也是要找n次,但是链表的n次操作主要在循环比较找位置,没有发生数据交换,而顺序表最坏的情况,就是每次插入都是第0个元素,其他所有元素后移,n次操作主要是n次移动数据,移动数据有内存开销,内存开销影响性能,n越来越大,数组需要的内存空间更大,如果遇到需要扩容性能更差,虽然复杂度都是O(n),n代表的含义不太一样,所以链表的性能更好,删除也是同样道理;
3.1.4 链表的反转
单链表反转
API设计:
public void reverse(); 整个链表反转
public Node reverse(Node node) ; 反转指定结点,A->B, 反转为B->A, 这里反转是指下一个元素反转入参是A,返回是B
/**
* 反转整个链表
* 这里是递归调用的,不太好理解
*/
public void reverse() {
if (isEmpty() || length() == 1) {
return;
}
reverse(head.next);
}
/**
* 反转链表,指定结点反转,返回值是反转之后的上一个结点
* @param current
* @return
*/
public Node reverse(Node current) {
if (current.next == null) {
head.next = current;
return current;
}
//递归调用,返回值是链表反转后的当前结点的上一个结点
Node prior = reverse(current.next);
prior.next = current;
//当前结点的下一个结点为null
current.next = null;
//返回上一个结点
return current;
}
3.1.5 快慢指针
定义两个指针,这两个指针的移动速度一快一慢,以此可以制造出自己想要的差值,这个差值可以让我们找到链表上相应的结点,一般情况下,快指针的移动步长为慢指针的两倍。
中间值问题
快指针移动两次, 慢指针移动一次, 快指针结束时,慢指针指向中间值;
public class FastSlow {
public static void main(String[] args) {
//构建结点
Node<Integer> first = new Node<Integer>(12,null);
Node<Integer> second = new Node<Integer>(13,null);
Node<Integer> third = new Node<Integer>(14,null);
Node<Integer> fourth = new Node<Integer>(15,null);
Node<Integer> fifth = new Node<Integer>(16,null);
Node<Integer> six = new Node<Integer>(17,null);
//生成链表
first.next =second;
second.next = third;
third.next = fourth;
fourth.next = fifth;
fifth.next = six;
//中间值
Integer mid= getMid(first);
System.out.println(mid);
}
/**
* 快慢指针解决中间值问题:
* 1 2 3 4 5 返回中间值3
* 1 2 3 4 5 6 返回 4
* @param first
* @return
*/
private static Integer getMid(Node<Integer> first) {
//定义两个指针
Node<Integer> fast = first;
Node<Integer> slow = first;
while(fast !=null && fast.next != null ) {
fast = fast.next.next;
slow = slow.next;
}
return slow.t;
}
}
单链表是否有环问题
有环链表 : 快指针会和慢指针相遇
分析过程 :
public class CheckCircleLink {
public static void main(String[] args) {
//构建结点
Node<Integer> first = new Node<Integer>(12,null);
Node<Integer> second = new Node<Integer>(13,null);
Node<Integer> third = new Node<Integer>(14,null);
Node<Integer> fourth = new Node<Integer>(15,null);
Node<Integer> fifth = new Node<Integer>(16,null);
Node<Integer> six = new Node<Integer>(17,null);
//生成链表
first.next =second;
second.next = third;
third.next = fourth;
fourth.next = fifth;
fifth.next = six;
//加环
six.next = fourth;
//检查是否有环
System.out.println(isCircle(first));
}
/**
* 快慢指针判断是否有环
* @param first
* @return
*/
private static boolean isCircle(Node<Integer> first) {
//定义两个指针
Node<Integer> fast = first;
Node<Integer> slow = first;
//如果两个指针指向对象一样就是有环
while (fast !=null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
if (slow.equals(fast)) return true;
}
return false;
}
}
环的入口问题:
当快慢指针相遇的时候,确定链表有环, 此时重新设定新指针直指向链表的起点,且步长与慢指针一样为1, 则慢指针与新指针,相遇的地方就是环的入口。 为什么呢?证明需要数学知识,不太会。
实际过程,看下图:
代码实现:
/**
* 查找环入口
* @param first
* @return
*/
private static Node getEntrance (Node<Integer> first) {
//定义两个指针
Node<Integer> fast = first;
Node<Integer> slow = first;
Node<Integer> temp = null;
//如果两个指针指向对象一样就是有环
while (fast !=null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
if (temp !=null) {
temp = temp.next;
if (temp.equals(slow)) {
break;
}
}
if (slow.equals(fast)) {
temp = first;
continue;
}
}
return temp;
}
循环链表
构建循环链表 : 头指向尾
public static void main(String[] args) {
//构建结点
Node<Integer> first = new Node<Integer>(12,null);
Node<Integer> second = new Node<Integer>(13,null);
Node<Integer> third = new Node<Integer>(14,null);
Node<Integer> fourth = new Node<Integer>(15,null);
Node<Integer> fifth = new Node<Integer>(16,null);
Node<Integer> six = new Node<Integer>(17,null);
//生成链表
first.next =second;
second.next = third;
third.next = fourth;
fourth.next = fifth;
fifth.next = six;
//加环
six.next = first;
}
约瑟夫问题
据说著名犹太历史学家Josephus有过以下的故事:在罗马人占领乔塔帕特后,39 个犹太人与Josephus及他的朋友躲到一个洞中,39个犹太人决定宁愿死也不要被敌人抓到,于是决定了一个自杀方式,41个人排成一个圆圈,由第1个人开始报数,每报数到第3人该人就必须自杀,然后再由下一个重新报数,直到所有人都自杀身亡为止。然而Josephus 和他的朋友并不想遵从。首先从一个人开始,越过k-2个人(因为第一个人已经被越过),并杀掉第k个人。接着,再越过k-1个人,并杀掉第k个人。这个过程沿着圆圈一直进行,直到最终只剩下一个人留下,这个人就可以继续活着。问题是,给定了和,一开始要站在什么地方才能避免被处决。Josephus要他的朋友先假装遵从,他将朋友与自己安排在第16个与第31个位置,于是逃过了这场死亡游戏。
17世纪的法国数学家加斯帕在《数目的游戏问题》中讲了这样一个故事:15个教徒和15 个非教徒在深海上遇险,必须将一半的人投入海中,其余的人才能幸免于难,于是想了一个办法:30个人围成一圆圈,从第一个人开始依次报数,每数到第九个人就将他扔入大海,如此循环进行直到仅余15个人为止。问怎样排法,才能使每次投入大海的都是非教徒。
构建循环列表 :
- 用41个结点,分别存储41个人
- 使用计数器count,记录当前报数的值
- 遍历链表,每次count++
- 判断count的值,如果是3,则从链表中删除这个结点,并打印,count重置为0
public class JosephusProblem {
public static void main(String[] args) {
//构建循环链表,存储41个数字
Node<Integer> next = null;
Node<Integer> first = null;
Node<Integer> end = null;
for (int i = 41; i > 0 ; i--) {
Node<Integer> node = new Node<Integer>(i,next);
if ( i == 41) end = node;
next = node;
}
first = next;
end.next = first;
//计数器
int count=0;
//当前结点
Node current = first;
//当前结点的上一个结点
Node prior = null;
//循环退出条件是自己指向自己
while (current != current.next) {
//模拟报数
count++;
//判断报数是不是3
if (count == 3) {
//如果是3就要删除当前结点,打印 重置count
prior.next = current.next;
System.out.print(current.t + " ");
count = 0;
current= current.next;
}else {
//将当前结点给上一个结点
prior = current;
//当前结点下移动
current = current.next;
}
}
//打印最一个人
System.out.println(current.t);
}
}
/**
输出结果
3 6 9 12 15 18 21 24 27 30 33 36 39 1 5 10 14 19 23 28 32 37 41 7 13 20 26 34 40 8 17 29 38 11 25 2 22 4 35 16 31
*/