目录
1 线性表简介
- 线性表是最基本、最简单、也是最常用的一种数据结构,一个线性表是若干个具有相同特性的数据元素的有限序列;
- 数据元素之间具有一种“一对一”的逻辑关系;
- 前驱元素:若A元素在B元素的前面,则称A为B的前驱元素;
- 后继元素:若B元素在A元素的后面,则称B为A的后继元素;
- 第一个数据元素没有前驱,这个数据元素被称为头结点;
- 最后一个数据元素没有后继,这个数据元素被称为尾结点;
- 除了第一个和最后一个数据元素外,其他数据元素有且仅有一个前驱和一个后继;
2 线性表分类
线性表数据存储的方式有两种:顺序存储和链式存储;
- 顺序存储:指用一组连续的存储单元存储线性表中的各个元素,使得线性表中在逻辑结构上相邻的数据元素在物理存储单元中也是相邻的;
- 链式存储:指用一组任意的存储单元存储线性表的数据元素,线性表中在逻辑结构上相邻的数据元素在物理存储单元中不一定是相邻的;
按照线性表数据存储方式的不同,可以把线性表分为顺序表(顺序存储)和链表(链式存储)。
3 顺序表
顺序表是在计算机内存中以数组的形式顺序存储的线性表。
3.1 API设计
类名 | SequenceList |
---|---|
构造方法 | SequenceList(int capacity):创建容量为capacity的SequenceList对象 |
成员方法 | 1.public void clear():空置线性表 2.public boolean isEmpty():判断线性表是否为空,是返回true,否返回false 3.public int length():获取线性表的长度 4.public T get(int i):返回线性表中索引i处的元素 5.public void insert(T t):向线性表中添加元素t 6.public void insert(int i,T t):在索引i处插入一个值为t的元素 7.public T remove(int i):删除并返回线性表中索引i处的元素 8.public int indexOf(T t):返回线性表中首次出现的指定元素的索引,若不存在,返回-1 9.public void resize(int newSize):根据参数newSize,重置线性表容量 10.public int capacity():返回线性表的容量 |
成员变量 | 1.private T[] eles:存储元素的数组 2.private int N:线性表的长度 |
3.1 Java代码
public class SequenceList<T> implements Iterable<T>{
//定义数组用来存储元素
private T[] eles;
//记录顺序表长度
private int N;
//构造方法
public SequenceList(int capacity){
//数组初始化容量大小为capacity
this.eles=(T[])new Object[capacity];
//线性表长度初始化为0
this.N=0;
}
//将线性表置为空
public void clear(){
this.N=0;
}
//判断线性表是否为空
public boolean isEmpty(){
return N==0;
}
//获取线性表长度
public int length(){
return N;
}
//获取索引i处的元素
public T get(int i){
if (i<0 || i>=N){
throw new RuntimeException("当前元素不存在!");
}
return eles[i];
}
//向线性表中添加元素t
public void insert(T t){
//元素已经放满了数组,需要扩容
if (N==eles.length){
resize(2*eles.length);
}
eles[N++]=t;
}
//向i索引处插入元素t
public void insert(int i,T t){
if (i<0 || i>N) {
throw new RuntimeException("插入的位置不合法");
}
//元素已经放满了数组,需要扩容
if (N==eles.length){
resize(2*eles.length);
}
//先把i索引处的元素及其后面的元素依次向后移动一位
for(int index=N;index>i;index--){
eles[index]=eles[index-1];
}
//再把t元素放到i索引处即可
eles[i]=t;
//线性表长度+1
N++;
}
//删除指定索引i处的元素,并返回该元素
public T remove(int i){
if (i<0 || i>N-1) {
throw new RuntimeException("当前要删除的元素不存在");
}
//记录索引i处的值
T current = eles[i];
//索引i后面元素依次向前移动一位即可
for(int index = i;index < N-1;index++){
eles[index] = eles[index+1];
}
//元素个数-1
N--;
//线性表长度已经不足线性表容量的1/4,减容
if (N<eles.length/4){
resize(eles.length/2);
}
return current;
}
//查找t元素第一次出现的位置
public int indexOf(T t){
if(t==null) {
throw new RuntimeException("查找的元素不合法");
}
for(int i=0;i<N;i++){
if (eles[i].equals(t)){
return i;
}
}
return -1;
}
//改变容量
public void resize(int newSize){
//记录旧数组
T[] temp=eles;
//创建新数组
eles=(T[])new Object[newSize];
//把旧数组的数据复制到新数组
for(int i=0;i<N;i++){
eles[i]=temp[i];
}
}
//获得线性表的容量
public int capacity() {
return eles.length;
}
@Override
public Iterator<T> iterator() {
return new SIterator();
}
private class SIterator implements Iterator{
private int cusor;
@Override
public boolean hasNext() {
return cusor<N;
}
@Override
public Object next() {
return eles[cusor++];
}
}
}
public class Test {
public static void main(String[] args) {
SequenceList<Integer> list = new SequenceList<>(4);
list.insert(1);
list.insert(2);
list.insert(3);
list.insert(4);
System.out.print(list.length() + "\t");System.out.println(list.capacity());
list.insert(5);
System.out.print(list.length() + "\t");System.out.println(list.capacity());
for(Integer i : list) {
System.out.print(i + " ");
}
list.clear();
for(Integer i : list) {
System.out.print(i + " ");
}
}
}
4 4
5 8
1 2 3 4 5
3.3 复杂性分析
- get(int i):不论顺序表长度N有多大,只要一次eles[i]即可获取对应元素,所以时间复杂度为O(1);
- insert(int i,T t):每一次插入,都需要把i及i后面的元素后移一次,随着N的增大,移动元素也越多,所以时间复杂度为O(N);
- remove(int i):每一次删除,都需要把i后面的元素前移一次,随着N的增大,移动元素也越多,所以时间复杂度为O(N);
- 由于顺序表的底层由数组实现,数组长度是固定的,因此会涉及容器扩容的操作,这就导致顺序表在使用中的时间复杂度不是线性的,在某些需要扩容的结点处,耗时会突增,尤其元素越多,这个问题越明显;
注:
- java中的ArrayList集合的底层就是一种顺序表,底层使用数组实现,也提供了增删改查以及扩容等功能。
4 链表
- 链表是在计算机内存中以结点的形式链式存储的线性表,在物理存储单元上非连续、非顺序;
- 链表由一系列结点组成,结点可以在运行时动态生成;
4.1 单向链表
4.1.1 简介
- 单向链表是链表的一种,由多个结点组成,每个结点由一个数据域和一个指针域构成,数据域用来存储数据,指针域用来指向其后继结点;
- 头结点不存储数据,指针域指向第一个真正存储数据的结点;
4.1.2 Java代码
public class LinkList<T> implements Iterable<T>{
//记录头结点
private Node head;
//记录链表的长度
private int N;
public LinkList() {
//初始化头结点
this.head = new Node(null,null);
//初始化元素个数
this.N = 0;
}
//向链表中添加元素t
public void insert(T t) {
//找到当前最后一个结点
Node pre = head;
while(pre.next!=null){
pre=pre.next;
}
//让当前最后一个结点指向新结点
pre.next = new Node(t, null);
//元素的个数+1
N++;
}
//向指定位置i处,添加元素t
public void insert(int i, T t) {
if (i<0 || i>=N) {
throw new RuntimeException("位置不合法!");
}
//找到i位置前一个结点
Node pre = head;
for(int index=0;index<i;index++){
pre = pre.next;
}
//新节点指向i位置节点称为i节点,原来i位置的前一个节点指向新结点
pre.next=new Node(t, pre.next);
//元素的个数+1
N++;
}
//删除指定位置i处的元素,并返回被删除的元素
public T remove(int i) {
if (i<0 || i>=N) {
throw new RuntimeException("位置不合法!");
}
//找到i位置的前一个节点
Node pre = head;
for(int index=0;index<i;index++){
pre=pre.next;
}
//要找到i位置的结点
Node curr = pre.next;
//前一个结点指向下一个结点
pre.next = curr.next;
//元素个数-1
N--;
return curr.item;
}
//清空链表
public void clear() {
head.next = null;
N = 0;
}
//获取指定位置i处的元素
public T get(int i) {
if (i<0 || i>=N) {
throw new RuntimeException("位置不合法!");
}
//通过循环,从头结点开始往后找,依次找i次,就可以找到对应的元素
Node curr = head;
for(int index=0;index<=i;index++){
curr=curr.next;
}
return curr.item;
}
//查找元素t在链表中第一次出现的位置
public int indexOf(T t) {
//从头结点开始,依次找到每一个结点,取出item,和t比较,如果相同,就找到了
Node pre = head;
int index = 0;
while(pre.next!=null) {
if(pre.next.item.equals(t)) {
return index;
}
pre = pre.next;
index++;
}
return -1;
}
//获取链表的长度
public int length() {
return N;
}
//判断链表是否为空
public boolean isEmpty() {
return N == 0;
}
// 设计迭代器
@Override
public Iterator<T> iterator() {
return new LIterator();
}
private class LIterator implements Iterator{
private Node n;
public LIterator(){
this.n=head;
}
@Override
public boolean hasNext() {
return n.next!=null;
}
@Override
public Object next() {
n = n.next;
return n.item;
}
}
//结点类
private class Node {
//存储数据
T item;
//下一个结点
Node next;
public Node(T item, Node next) {
this.item = item;
this.next = next;
}
}
}
public class Test {
public static void main(String[] args) {
LinkList<Integer> list = new LinkList<>();
list.insert(1);
list.insert(2);
list.insert(3);
list.insert(4);
for(Integer i : list) {
System.out.print(i + " ");
}
}
}
1 2 3 4
4.2 双向链表
4.2.1 简介
- 双向链表是链表的一种,由多个结点组成,每个结点由一个数据域和两个指针域构成;
- 数据域用来存储数据,其中一个指针域用来指向其后继结点,另一个指针域用来指向其前驱结点;
- 链表头结点的数据域不存储数据,指向前驱结点的指针域值为null,指向后继结点的指针域指向第一个真正存储数据的结点;
4.2.2 Java代码
public class TwoWayLinkList<T> implements Iterable<T> {
//头结点
private Node head;
//最后一个结点
private Node last;
//链表长度
private int N;
//结点类
private class Node{
public Node(T item, Node pre, Node next) {
this.item = item;
this.pre = pre;
this.next = next;
}
//存储数据
public T item;
//指向上一个结点
public Node pre;
//指向下一个结点
public Node next;
}
public TwoWayLinkList() {
//初始化头结点和尾结点
head = new Node(null,null,null);
last=null;
//初始化元素个数
N=0;
}
//清空链表
public void clear(){
head.next=null;
head.pre=null;
head.item=null;
last=null;
N=0;
}
//获取链表长度
public int length(){
return N;
}
//判断链表是否为空
public boolean isEmpty(){
return N==0;
}
//获取第一个元素
public T getFirst(){
if (isEmpty()){
return null;
}
return head.next.item;
}
//获取最后一个元素
public T getLast(){
if (isEmpty()){
return null;
}
return last.item;
}
//插入元素t
public void insert(T t){
if (isEmpty()){
//如果链表为空,创建新的结点,并让新结点成为尾结点
last = new Node(t, head, null);
//让头结点指向尾结点
head.next = last;
}else {//如果链表不为空
//让尾结点成为旧的尾结点
Node oldLast = last;
//创建新的尾结点
Node newLast = new Node(t, oldLast, null);
//让旧的尾结点指向新的尾结点
oldLast.next = newLast;
//让新结点成为尾结点
last = newLast;
}
//长度+1
N++;
}
//向指定位置i处插入元素t
public void insert(int i,T t){
if (i < 0 || i >= N) {
throw new RuntimeException("位置不合法");
}
//找到i位置的前一个结点
Node pre = head;
for(int index=0; index<i; index++){
pre = pre.next;
}
//找到i位置的结点
Node curr = pre.next;
//创建新结点
Node newNode = new Node(t, pre, curr);
pre.next = newNode;
curr.pre = newNode;
//元素个数+1
N++;
}
//获取指定位置i处的元素
public T get(int i){
if (i < 0 || i >= N) {
throw new RuntimeException("位置不合法");
}
//寻找当前结点
Node curr = head.next;
for(int index=0; index<i; index++){
curr = curr.next;
}
return curr.item;
}
//找到元素t在链表中第一次出现的位置
public int indexOf(T t){
Node n = head;
for(int i=0; n.next!=null; i++){
if (n.next.item.equals(t)){
return i;
}
n = n.next;
}
return -1;
}
//删除位置i处的元素,并返回该元素
public T remove(int i){
if (i < 0 || i >= N) {
throw new RuntimeException("位置不合法");
}
//找到i位置的前一个结点
Node pre = head;
for(int index=0; index<i; index++){
pre = pre.next;
}
//找到i位置的结点
Node curr = pre.next;
//找到i位置的下一个结点
Node curr_next = curr.next;
pre.next = curr_next;
curr_next.pre = pre;
//长度-1
N--;
return curr.item;
}
@Override
public Iterator<T> iterator() {
return new TIterator();
}
private class TIterator implements Iterator{
private Node n = head;
@Override
public boolean hasNext() {
return n.next!=null;
}
@Override
public Object next() {
n = n.next;
return n.item;
}
}
}
public class Test {
public static void main(String[] args) {
TwoWayLinkList<String> list = new TwoWayLinkList<>();
list.insert("乔峰");
list.insert("虚竹");
list.insert("段誉");
for (String str : list) {
System.out.println(str);
}
System.out.println(list.indexOf("段誉"));
}
}
乔峰
虚竹
段誉
2
4.3 复杂性分析
- get(int i):每一次查询,都需要从链表的头部开始,依次向后查找,随着数据元素N的增多,比较的元素也将增多,时间复杂度为O(N);
- insert(int i,T t):每一次插入,需要先找到i位置的前一个元素,然后完成插入操作,随着数据元素N的增多,查找的元素也将增多,时间复杂度为O(N);
- remove(int i):每一次移除,需要先找到i位置的前一个元素,然后完成删除操作,随着数据元素N的增多,查找的元素也将增多,时间复杂度为O(N);
注:
- java中的LinkedList集合就是使用双向链表实现,并提供了增删改查等相关方法。
4.4 链表VS顺序表
- 相比于顺序表,链表插入和删除的时间复杂度虽然也都是O(N),但依然具有较大的优势:链表物理地址不连续,因此不需要预先指定存储空间大小,也不会涉及到扩容等操作,同时也不会涉及元素的交换;
- 相比于顺序表,链表查询时间复杂度为O(N),而顺序表时间复杂度为O(1),性能差距明显,主要是因为顺序表的物理地址连续,链表不连续;
- 由上可知,如果在程序中查询操作比较多,建议使用顺序表,如果增删操作比较多,建议使用链表;
4.5 链表反转
需求:
原链表中数据为:1->2->3>4
反转后链表中数据为:4->3->2->1
反转API:
public void reverse()
:对整个链表反转;public Node reverse(Node cur)
:反转链表中的某个结点curr,并把反转后的curr结点返回;
使用递归可以完成反转,递归反转就是从链表的第一个存数据的结点开始,依次递归调用反转每一个结点,直到把最后一个结点反转完毕,整个链表就反转完毕。
//反转链表
public void reverse(){
//判断当前链表是否为空链表,如果是空链表,则结束运行,如果不是,则调用重载的reverse方法完成反转
if (isEmpty()){
return;
}
reverse(head.next);
}
//反转指定的结点curr,并把反转后的结点返回
public Node reverse(Node curr){
if (curr.next==null){
head.next=curr;
return curr;
}
//递归反转当前结点curr的下一个结点
Node pre = reverse(curr.next);
//让返回的结点的下一个结点变为当前结点curr
pre.next=curr;
//把当前结点的下一个结点变为null
curr.next=null;
return curr;
}
public class Test {
public static void main(String[] args) {
LinkList<Integer> list = new LinkList<>();
list.insert(1);
list.insert(2);
list.insert(3);
list.insert(4);
for(Integer i : list) {
System.out.print(i + " ");
}
System.out.println();
System.out.println("----------------");
list.reverse();
for(Integer i : list) {
System.out.print(i + " ");
}
}
}
1 2 3 4
----------------
4 3 2 1
4.6 快慢指针
- 快慢指针指的是定义两个指针,这两个指针的移动速度一快一慢,以此制造出我们想要的差值,这个差值可以帮我们找到链表上响应的结点;
- 一般情况下,快指针移动步长为慢指针的两倍;
4.6.1 中间值问题
需求
找出链表的中间元素
思路
利用快慢指针,我们把一个链表看成一个跑道,假设a的速度是b的两倍,那么当a跑完全程后,b刚好跑一半,以此来达到找到中间节点的目的。
public class Test {
public static void main(String[] args) {
// 定义结点
Node<String> first = new Node<>("aa", null);
Node<String> second = new Node<>("bb", null);
Node<String> third = new Node<>("cc", null);
Node<String> fourth = new Node<>("dd", null);
Node<String> fifth = new Node<>("ee", null);
Node<String> sixth = new Node<>("ff", null);
Node<String> seventh = new Node<>("gg", null);
// 完成结点之间的指向
first.next = second;
second.next = third;
third.next = fourth;
fourth.next = fifth;
fifth.next = sixth;
sixth.next = seventh;
// 查找中间值
String mid = getMid(first);
System.out.println("中间值为:" + mid);
}
// 定义查找中间值的函数
public static String getMid(Node<String> first) {
Node<String> slow = first;
Node<String> fast = first;
while(fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
}
return slow.item;
}
// 结点类
private static class Node<T> {
// 存储数据
T item;
// 下一个结点
Node next;
public Node(T item, Node next) {
this.item = item;
this.next = next;
}
}
}
中间值为:dd
4.6.2 单向链表是否有环问题
需求
判断链表是否有环
思路
利用快慢指针的思想,把链表比作一条跑道,链表中有环,那这条跑道就是一条圆环跑道,在一条圆环跑道中,两个人有速度差,两个人迟早会相遇,只要相遇就说明有环。
public class Test {
public static void main(String[] args) {
// 定义结点
Node<String> first = new Node<>("aa", null);
Node<String> second = new Node<>("bb", null);
Node<String> third = new Node<>("cc", null);
Node<String> fourth = new Node<>("dd", null);
Node<String> fifth = new Node<>("ee", null);
Node<String> sixth = new Node<>("ff", null);
Node<String> seventh = new Node<>("gg", null);
// 完成结点之间的指向
first.next = second;
second.next = third;
third.next = fourth;
fourth.next = fifth;
fifth.next = sixth;
sixth.next = seventh;
// 产生环
seventh.next = third;
// 判断链表是否有环
boolean circle = isCircle(first);
System.out.println("first链表中是否有环:" + circle);
}
// 定义判断链表是否有环的函数
public static boolean isCircle(Node<String> first) {
Node<String> slow = first;
Node<String> fast = first;
while (fast != null && fast.next != null) {
if (fast.next == null) return false;
fast = fast.next.next;
slow = slow.next;
if (fast == slow) {
return true;
}
}
return false;
}
// 结点类
private static class Node<T> {
// 存储数据
T item;
// 下一个结点
Node next;
public Node(T item, Node next) {
this.item = item;
this.next = next;
}
}
}
first链表中是否有环:true
4.6.3 有环链表入口问题
需求
查找有环链表中环的入口结点
思路
- 当快慢指针相遇时,可以判断链表中有环;
- 这时重新设定一个新指针指向链表起点,且步长与慢指针一样为1,则慢指针与新指针相遇的地方就是环的入口;
- 证明这一结论涉及到数论的知识,略,只讲实现;
public class Test {
public static void main(String[] args) {
// 定义结点
Node<String> first = new Node<>("aa", null);
Node<String> second = new Node<>("bb", null);
Node<String> third = new Node<>("cc", null);
Node<String> fourth = new Node<>("dd", null);
Node<String> fifth = new Node<>("ee", null);
Node<String> sixth = new Node<>("ff", null);
Node<String> seventh = new Node<>("gg", null);
// 完成结点之间的指向
first.next = second;
second.next = third;
third.next = fourth;
fourth.next = fifth;
fifth.next = sixth;
sixth.next = seventh;
// 产生环
seventh.next = third;
// 查找环的入口结点
Node<String> entrance = getEntrance(first);
System.out.println("first链表中环的入口结点元素为:" + entrance.item);
}
// 查找有环链表中环的入口结点
public static Node getEntrance(Node<String> first) {
Node<String> slow = first;
Node<String> fast = first;
Node<String> temp = null;
while (fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
if (fast == slow) {
temp = first;
continue;
}
if (temp != null) {
temp = temp.next;
if (temp == slow) {
return temp;
}
}
}
return temp;
}
// 结点类
private static class Node<T> {
// 存储数据
T item;
// 下一个结点
Node next;
public Node(T item, Node next) {
this.item = item;
this.next = next;
}
}
}
first链表中环的入口结点元素为:cc
4.7 循环链表
- 循环链表,即链表要形成一个圆环状;
- 在单向链表中,最后一个节点的next指针为null,不指向任何节点;
- 要实现循环链表,只需要让单向链表的最后一个节点的next指针指向第一个节点即可;
循环链表的构建
public class Test {
public static void main(String[] args) {
// 定义结点
Node<Integer> first = new Node<>(1, null);
Node<Integer> second = new Node<>(2, null);
Node<Integer> third = new Node<>(3, null);
Node<Integer> fourth = new Node<>(4, null);
Node<Integer> fifth = new Node<>(5, null);
Node<Integer> sixth = new Node<>(6, null);
Node<Integer> seventh = new Node<>(7, null);
// 构建单链表
first.next = second;
second.next = third;
third.next = fourth;
fourth.next = fifth;
fifth.next = sixth;
sixth.next = seventh;
// 构建循环链表,让最后一个节点指向第一个节点
seventh.next = first;
}
// 结点类
private static class Node<T> {
// 存储数据
T item;
// 下一个结点
Node next;
public Node(T item, Node next) {
this.item = item;
this.next = next;
}
}
}
约瑟夫问题
问题描述
传说有这样一个故事,在罗马人占领乔塔帕特后,39 个犹太人与约瑟夫及他的朋友躲到一个洞中,39个犹太人决定宁愿死也不要被敌人抓到,于是决定了一个自杀方式,41个人排成一个圆圈,第一个人从1开始报数,依次往后,如果有人报数到3,那么这个人就必须自杀,然后再由他的下一个人重新从1开始报数,直到所有人都自杀身亡为止。然而约瑟夫和他的朋友并不想遵从。于是,约瑟夫要他的朋友先假装遵从,他将朋友与自己安排在第16个与第31个位置,从而逃过了这场死亡游戏 。
问题转换
41个人坐一圈,第一个人编号为1,第二个人编号为2,第n个人编号为n:
- 编号为1的人开始从1报数,依次向后,报数为3的那个人退出圈;
- 自退出那个人开始的下一个人再次从1开始报数,以此类推;
- 求出最后退出的两个人的编号;
解题思路
- 构建含有41个结点的循环链表,分别存储1~41的值,分别代表这41个人;
- 使用计数器count,记录当前报数的值;
- 遍历链表,每循环一次,count++;
- 判断count的值,如果是3,则从链表中删除这个结点并打印结点的值,把count重置为0;
代码
public class Test {
public static void main(String[] args) {
//1.构建循环链表
Node<Integer> first = null;
//记录前一个结点
Node<Integer> pre = null;
for (int i=1; i <=41; i++) {
//第一个元素
if (i == 1) {
first = new Node(i, null);
pre = first;
continue;
}
Node<Integer> node = new Node<>(i, null);
pre.next = node;
pre = node;
if (i == 41) {
pre.next = first;
}
}
//2.使用count,记录当前的报数值
int count = 0;
//3.遍历链表,每循环一次,count++
Node<Integer> n = first;
Node<Integer> before = null;
while (n != n.next.next) {
//4.判断count的值,如果是3,则从链表中删除这个结点并打印结点的值,把count重置为0
count++;
if (count==3) {
//删除当前结点
before.next = n.next;
System.out.print(n.item + ",");
count = 0;
n = n.next;
} else {
before = n;
n = n.next;
}
}
//打印剩余的最后两个人
System.out.println("\n" + n.item + "," + n.next.item);
}
// 结点类
private static class Node<T> {
// 存储数据
T item;
// 下一个结点
Node next;
public Node(T item, Node next) {
this.item = item;
this.next = next;
}
}
}
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