1. 线性表
线性表是最基本、最简单、也是最常用的一种数据结构。一个线性表是n个具有相同特性的数据元素的有限序列。
前驱元素若A元素在B元素的前面,则称A为B的前驱元素
后继元素若B元素在A元素的后面,则称B为A的后继元素
线性表的特征元素之间具有“一对一”的逻辑关系
- 第一个元素没有前驱元素,被称为头结点
- 最后一个数据元素没有后继,这个数据元素被称为尾结点
- 其余中间元素有且只有一个前驱元素和后继元素
1.1 顺序表
顺序表是在计算机内存中以数组的形式保存的线性表,线性表的顺序存储是指用一组地址连续的存储单元,依次存
储线性表中的各个元素、使得线性表中再逻辑结构上响铃的数据元素存储在相邻的物理存储单元中,即通过数据元
素物理存储的相邻关系来反映数据元素之间逻辑上的相邻关系。
1.1.1 顺序表的实现
代码实现
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(){
N=0;
}
//判断线性表是否为空,是返回true,否返回false
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];
}
//在线性表的第i个元素之前插入一个值为t的数据元素。
public void insert(int i,T t){
if (i<0||i>N){
throw new RuntimeException("当前元素不存在");
}
if (N==eles.length){
resize(eles.length*2);
}
for (int index=N-1;index>i;index--){
eles[index]=eles[index-1];
}
eles[i]=t;
N++;
}
//线性表扩容
private void resize(int newSize) {
T[] temp=eles;
eles = (T[]) new Object[newSize];
for (int i=0;i<N;i++){
eles[i]=temp[i];
}
}
//向线性表中添加一个元素t
public void insert(T t){
if (N==eles.length){
resize(eles.length*2);
}
eles[N]=t;
N++;
}
//删除并返回线性表中第i个数据元素。
public T remove(int i){
if (i<=0||i>N-1||N==0){
throw new RuntimeException("要删除的元素不存在");
}
T result = eles[i];
for (int index=i;index<N-1;index++){
eles[index]=eles[index+1];
}
N--;
return result;
}
//返回线性表中首次出现的指定的数据元素的位序号,若不存在,则返回-1。
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;
}
1.1.2 顺序表的时间复杂度
get(i):不难看出,不论数据元素量N有多大,只需要一次eles[i]就可以获取到对应的元素,所以时间复杂度为O(1);
insert(int i,T t):每一次插入,都需要把i位置后面的元素移动一次,随着元素数量N的增大,移动的元素也越多,时
间复杂为O(n);
remove(int i):每一次删除,都需要把i位置后面的元素移动一次,随着数据量N的增大,移动的元素也越多,时间复
杂度为O(n);
由于顺序表的底层由数组实现,数组的长度是固定的,所以在操作的过程中涉及到了容器扩容操作。这样会导致顺
序表在使用过程中的时间复杂度不是线性的,在某些需要扩容的结点处,耗时会突增,尤其是元素越多,这个问题
越明显
1.1.3 java中ArrayList实现
java中ArrayList的底层也是顺序表,使用数组实现,并且提供了增删改查等1及扩容功能
1.2 链表
之前我们已经使用顺序存储结构实现了线性表,我们会发现虽然顺序表的查询很快,时间复杂度为O(1),但是增删的
效率是比较低的,因为每一次增删操作都伴随着大量的数据元素移动。这个问题有没有解决方案呢?有,我们可以
使用另外一种存储结构实现线性表,链式存储结构。
链表是一种物理存储单元上非连续、非顺序的存储结构,其物理结构不能只管的表示数据元素的逻辑顺序,数据元
素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列的结点(链表中的每一个元素称为结点)组成,
结点可以在运行时动态生成。
1.2.1 单链表
单向链表是链表的一种,它由多个结点组成,每个结点都由一个数据域和一个指针域组成,数据域用来存储数据,
指针域用来指向其后继结点。链表的头结点的数据域不存储数据,指针域指向第一个真正存储数据的结点。
1.2.2 单链表代码实现
package demo;
import java.util.Iterator;
//单链表
public class LinkList<T> implements Iterable<T>{
//记录头结点
private Node head;
//记录链表的长度
private int N;
//结点类
private class Node {
//存储数据
T item;
//下一个结点
Node next;
public Node(T item, Node next) {
this.item = item;
this.next = next;
}
}
public LinkList() {
//初始化头结点、
this.head = new Node(null,null);
//初始化元素个数
this.N=0;
}
//清空链表
public void clear() {
head.next=null;
this.N=0;
}
//获取链表的长度
public int length() {
return N;
}
//判断链表是否为空
public boolean isEmpty() {
return N==0;
}
//获取指定位置i出的元素
public T get(int i) {
//通过循环,从头结点开始往后找,依次找i次,就可以找到对应的元素
Node n = head.next;
for(int index=0;index<i;index++){
n=n.next;
}
return n.item;
}
//向链表中添加元素t
public void insert(T t) {
//找到当前最后一个结点
Node n = head;
while(n.next!=null){
n=n.next;
}
//创建新结点,保存元素t
Node newNode = new Node(t, null);
//让当前最后一个结点指向新结点
n.next=newNode;
//元素的个数+1
N++;
}
//向指定位置i出,添加元素t
public void insert(int i, T t) {
//找到i位置前一个结点
Node pre = head;
for(int index=0;index<=i-1;index++){
pre=pre.next;
}
//找到i位置的结点
Node curr = pre.next;
//创建新结点,并且新结点需要指向原来i位置的结点
Node newNode = new Node(t, curr);
//原来i位置的前一个节点指向新结点即可
pre.next=newNode;
//元素的个数+1
N++;
}
//删除指定位置i处的元素,并返回被删除的元素
public T remove(int i) {
//找到i位置的前一个节点
Node pre = head;
for(int index=0;index<=i-1;i++){
pre=pre.next;
}
//要找到i位置的结点
Node curr = pre.next;
//找到i位置的下一个结点
Node nextNode = curr.next;
//前一个结点指向下一个结点
pre.next=nextNode;
//元素个数-1
N--;
return curr.item;
}
//查找元素t在链表中第一次出现的位置
public int indexOf(T t) {
//从头结点开始,依次找到每一个结点,取出item,和t比较,如果相同,就找到了
Node n = head;
for(int i=0;n.next!=null;i++){
n=n.next;
if (n.item.equals(t)){
return i;
}
}
return -1;
}
@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;
}
}
//用来反转整个链表
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;
}
}
1.2.3 双向链表
双向链表也叫双链表,是链表的一种,它由多个结点组成,每个结点都由一个数据域喝两个指针域组成,数据域用来存储数据,其中一个指针域用来指向其后继结点,另一个指针域用来指定前驱结点。链表的头结点的数据域不存储数据,指向前驱结点的指针域值为null,指向后继结点的指针域指向第一个真正存储数据的结点。
1.2.3.1 代码实现
//双向链表
public class TowWayLinkList<T> implements Iterable<T> {
//头结点
private Node head;
//尾结点
private Node last;
//链表长度
private int N;
public class Node{
T item;
Node next;
Node pre;
public Node(T t,Node pre,Node next){
this.item=t;
this.next=next;
this.pre=pre;
}
}
public TowWayLinkList(){
this.last=null;
this.head=new Node(null,null,null);
this.N=0;
}
//空置线性表
public void clear(){
this.last=null;
head.pre=null;
head.next=null;
head.item=null;
N=0;
}
//判断线性表是否为空,是返回true,否返回false
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("位置不合法");
}
Node curr = head.next;
for (int index = 0; index <i ; index++) {
curr=curr.next;
}
return curr.item;
}
//往线性表中添加一个元素
public void insert(T t){
if (last==null){
last=new Node(t,head,null);
head.next=last;
}else {
Node oldLast = this.last;
Node newLast = new Node(t, oldLast, null);
oldLast.next=newLast;
last=newLast;
}
N++;
}
//在线性表的第i个元素之前插入一个值为t的数据元素
public void insert(int i,T t){
if (i<0||i>N){
throw new RuntimeException("位置不合法");
}
//先找到位置i的前一个结点
Node pre = this.head;
for (int index = 0; index <i ; index++) {
pre=pre.next;
}
//当前结点
Node curr = pre.next;
Node node = new Node(t, pre, curr);
curr.pre=node;
pre.next=node;
N++;
}
//删除并返回线性表中第i个数据元素
public T remove(int i){
if (i<0||i>N){
throw new RuntimeException("位置不合法");
}
//删除位置的前一个结点
Node pre = head;
for (int index = 0; index <i ; index++) {
pre=pre.next;
}
//当前结点
Node curr = pre.next;
pre.next=curr.next;
curr.next.pre=pre;
N--;
return curr.item;
}
//返回线性表中首次出现的指定的数据元素的位序号,若不存在,则返回-1
public int indexOf(T t){
if (t==null){
throw new RuntimeException("元素不存在");
}
Node node = this.head;
for (int i = 0; i < N; i++) {
node=node.next;
if (node.item==t){
return i;
}
}
return -1;
}
//获取第一个元素
public T getFirst(){
if (isEmpty()){
return null;
}
return head.next.item;
}
//获取最后一个元素
public T getLast(){
if (isEmpty()){
return null;
}
return last.item;
}
1.2.3.2 java中LinkedList实现
java中ListedList集合也是使用双向链表实现,并提供增删改查的方法
1.2.3 链表的复杂度分析
get(int i):每一次查询,都需要从链表的头部开始,依次向后查找,随着数据元素N的增多,比较的元素越多,时间
复杂度为O(n)
insert(int i,T t):每一次插入,需要先找到i位置的前一个元素,然后完成插入操作,随着数据元素N的增多,查找的
元素越多,时间复杂度为O(n);
remove(int i):每一次移除,需要先找到i位置的前一个元素,然后完成插入操作,随着数据元素N的增多,查找的元
素越多,时间复杂度为O(n)
相比较顺序表,链表插入和删除的时间复杂度虽然一样,但仍然有很大的优势,因为链表的物理地址是不连续的,
它不需要预先指定存储空间大小,或者在存储过程中涉及到扩容等操作,同时它并没有涉及的元素的交换。
相比较顺序表,链表的查询操作性能会比较低。因此,如果我们的程序中查询操作比较多,建议使用顺序表,增删
操作比较多,建议使用链表。
1.2.4 快慢指针
快慢指针指的是定义两个指针,这两个指针的移动速度一块一慢,以此来制造出自己想要的差值,这个差值可以然
我们找到链表上相应的结点。一般情况下,快指针的移动步长为慢指针的两倍
1.2.4.1 中间值问题
利用快慢指针找出中间值
分析:
利用快慢指针,我们把一个链表看成一个跑道,假设a的速度是b的两倍,那么当a跑完全程后,b刚好跑一半,以
此来达到找到中间节点的目的。
如下图,最开始,slow与fast指针都指向链表第一个节点,然后slow每次移动一个指针,fast每次移动两个指针。
代码实现:
/**
* @param first 链表的首结点
* @return 链表的中间结点的值
*/
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;
}
1.2.4.2 单向链表是否有环问题
需求分析:
使用快慢指针的思想,还是把链表比作一条跑道,链表中有环,那么这条跑道就是一条圆环跑道,在一条圆环跑道
中,两个人有速度差,那么迟早两个人会相遇,只要相遇那么就说明有环。
代码
/**
* 判断链表中是否有环
* @param first 链表首结点
* @return ture为有环,false为无环
*/
public static boolean isCircle(Node<String> first) {
Node<String> slow = first;
Node<String> fast = first;
while(fast!=null && fast.next!=null){
fast = fast.next.next;
slow = slow.next;
if (fast.equals(slow)){
return true;
}
}
return false;
}
1.2.4.3 有环链表入口问题
需求分析:
当快慢指针相遇时,我们可以判断到链表中有环,这时重新设定一个新指针指向链表的起点,且步长与慢指针一样
为1,则慢指针与“新”指针相遇的地方就是环的入口。
代码实现:
/**
* 查找有环链表中环的入口结点
* @param first 链表首结点
* @return 环的入口结点
*/
public static Node getEntrance(Node<String> first) {
Node<String> fast=first;
Node<String> slow=first;
Node<String> temp=null;
while(fast!=null&&fast.next!=null){
fast=fast.next;
slow=slow.next;
if(fast.equals(slow)){
temp=first;
continue;
}
if(temp!=null){
temp=temp.next;
if(temp.equals(slow)){
return temp;
}
}
}
return null;
}
1.2.5 循环链表
循环链表,顾名思义,链表整体要形成一个圆环状。在单向链表中,最后一个节点的指针为null,不指向任何结
点,因为没有下一个元素了。要实现循环链表,我们只需要让单向链表的最后一个节点的指针指向头结点即可。
代码构建:
public class Test{
public static void main(String [] args) throws Exception{
//构建
Node<Integer> first = new Node<Integer>(1, null);
Node<Integer> second = new Node<Integer>(2, null);
Node<Integer> third = new Node<Integer>(3, null);
Node<Integer> fourth = new Node<Integer>(4, null);
Node<Integer> fifth = new Node<Integer>(5, null);
Node<Integer> six = new Node<Integer>(6, null);
Node<Integer> seven = new Node<Integer>(7, null);
//构建单链表
first.next = second;
second.next = third;
third.next = fourth;
fourth.next = fifth;
fifth.next = six;
six.next = seven;
//构建循环链表,让最后一个结点指向第一个结点
seven.next = first;
}
}
1.2.6 约瑟夫问题
问题描述:
传说有这样一个故事,在罗马人占领乔塔帕特后,39 个犹太人与约瑟夫及他的朋友躲到一个洞中,39个犹太人决
定宁愿死也不要被敌人抓到,于是决定了一个自杀方式,41个人排成一个圆圈,第一个人从1开始报数,依次往
后,如果有人报数到3,那么这个人就必须自杀,然后再由他的下一个人重新从1开始报数,直到所有人都自杀身亡
为止。然而约瑟夫和他的朋友并不想遵从。于是,约瑟夫要他的朋友先假装遵从,他将朋友与自己安排在第16个与
第31个位置,从而逃过了这场死亡游戏 。
问题转换:
41个人坐一圈,第一个人编号为1,第二个人编号为2,第n个人编号为n。
1.编号为1的人开始从1报数,依次向后,报数为3的那个人退出圈;
2.自退出那个人开始的下一个人再次从1开始报数,以此类推;
3.求出最后退出的那个人的编号
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cG0WVx1u-1593439959573)(https://i.loli.net/2020/06/29/sYFBoGqjKx4vWZN.png)]
解决思路:
- 构建含有41个结点的单向循环链表,分别存储1~41的值,分别代表这41个人;
- 使用计数器count,记录当前报数的值;
- 遍历链表,每循环一次,count++;
- 判断count的值,如果是3,则从链表中删除这个结点并打印结点的值,把count重置为0;
//约瑟夫问题
public class YueSeFu {
public static void main(String[] args) {
//1.构建循环链表,包含41个结点,分别存储1~41之间的值
//用来记录首结点
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> newNode = new Node<>(i, null);
pre.next=newNode;
pre=newNode;
//如果是最后一个结点,直接指向第一个结点,形成循环链表
if (i==41){
pre.next=first;
}
}
//2. 需要count计数器,模拟报数
int count=0;
//3.遍历循环链表
//记录每次遍历拿到的结点,默认从首结点开始
Node<Integer> n = first;
//记录当前结点的上一个结点
Node<Integer> before = null;
while (n!=n.next){
//模拟报数
count++;
if (count==3){
before.next = n.next;
System.out.print(n.items+" ");
count=0;
n=n.next;
}else {
before=n;
n=n.next;
}
}
//打印最后一个元素
System.out.print(n.items);
}
//结点类
private static class Node<T>{
T items;
Node next;
public Node(T items,Node next){
this.items=items;
this.next=next;
}
}
}