目录
目录
1、线性表概述
线性表是最基本、最简单、也是最常用的一种数据结构。一个线性表是n个具有相同特性的数据元素的有限序列。
例:A B C D
前驱元素:A在B前面,则称A是B的前驱元素
后继元素:B在A后面,则称B是A的后继元素
线性表的特征:数据元素之间具有一种“一对一”的逻辑关系。
1 . 第一个数据元素没有前驱,这个数据元素被称为头结点;
2. 最后一个数据元素没有后继,这个数据元素被称为尾结点;
3. 除了第一个和最后一个数据元素外,其他数据元素有且仅有一个前驱和一个后继。
线性表的分类:
线性表中数据存储的方式可以是顺序存储,也可以是链式存储,按照数据的存储方式不同,可以把线性表分为顺序表和链表。
2、顺序表测试
顺序表:查询快,增删慢(ArrayList)
SequenceList:
package xianxing;
import java.util.Iterator;
//创建顺序表容器
public class SequenceList<T> implements Iterable<T>{
//存储元素的数组
private T[] arr;
//记录当前线性表的元素个数
private int N;
public SequenceList(int capacity){
this.arr = (T[]) new Object[capacity];
this.N = 0;
}
//将一个线性表设置为空表
public void clear() {
this.N = 0;
}
//判断当前线性表是否为空表
public boolean isEmpty() {
return N==0;
}
//获取当前线性表的长度
public int length() {
return N;
}
//获取指定位置的元素
public T get(int i) {
return arr[i];
}
//向线性表中添加元素
public void insert(T t) {
arr[N++] = t;
}
//在指定索引处插入数据
public void insert(int i, T t) {
for(int j=N; j>i; j--) {
arr[j] = arr[j-1];
}
arr[i] = t;
N++;
}
//删除指定索引处的元素,并返回该元素
public T remove(int i) {
T m = arr[i];
for(int j=i; j<N-1; j++) {
arr[j] = arr[j+1];
}
return m;
}
//查找元素t第一次出现的位置
public int indexof(T t) {
for(int j=0; j<N-1; j++) {
if(arr[j].equals(t)) {
return j;
}
}
return -1;
}
//遍历
@Override
public Iterator<T> iterator() {
return new SIterator();
}
private class SIterator implements Iterator{
//定义一个指针
private int cusor;
public SIterator() {
this.cusor = 0;
}
@Override
public boolean hasNext() {
return cusor<N;
}
@Override
public Object next() {
return arr[cusor++];
}
}
}
SequenceTest
package xianxing;
//顺序表容器测试
public class SequenceTest {
public static void main(String[] args) {
SequenceList<String> sl = new SequenceList<>(10);
System.out.println("是否为空表"+sl.isEmpty());
sl.insert("李四");
sl.insert("王五");
sl.insert("李四");
sl.insert(0, "张三");
for(String s : sl) {
System.out.println(s);
}
System.out.println("第一个元素为:"+sl.get(0));
System.out.println("删除的元素为:"+sl.remove(0));
System.out.println("'李四'第一次出现的位置为:"+sl.indexof("李四"));
}
}
3、顺序表容量可变
存在的问题:当元素数量超过容量时,会报错。
SequenceList新增代码
//根据newSize,重置arr大小
public void reSize(int newSize) {
//定义一个临时数组,指向arr
T[] temp = arr;
arr = (T[]) new Object[newSize];
for(int i=0; i<N; i++) {
arr[i] = temp[i];
}
}
1)扩容
当前元素数量达到容量最大值,新建一个数组,容量是原来的2倍,将原来的元素放进去。
修改:
//向线性表中添加元素
public void insert(T t) {
if(N==arr.length) {
reSize(arr.length*2);
}
arr[N++] = t;
}
//在指定索引处插入数据
public void insert(int i, T t) {
if(N==arr.length) {
reSize(arr.length*2);
}
for(int j=N; j>i; j--) {
arr[j] = arr[j-1];
}
arr[i] = t;
N++;
}
2)缩容
当前元素数量小于容量的1/4,容量缩小到原来的1/2。
//删除指定索引处的元素,并返回该元素
public T remove(int i) {
T m = arr[i];
for(int j=i; j<N-1; j++) {
arr[j] = arr[j+1];
}
N--;
if(N<arr.length/4) {
reSize(arr.length/2);
}
return m;
}
4、顺序表的时间复杂度
get(i):不难看出,不论数据元素量N有多大,只需要一次eles[i]就可以获取到对应的元素,所以时间复杂度为O(1);insert(int i,T t):每一次插入,都需要把i位置后面的元素移动一次,随着元素数量N的增大,移动的元素也越多,间复杂为O(n);
remove(int i):每一次删除,都需要把i位置后面的元素移动一次,随着数据量N的增大,移动的元素也越多,时间复杂度为O(n);
由于顺序表的底层由数组实现,数组的长度是固定的,所以在操作的过程中涉及到了容器扩容操作。这样会导致顺序表在使用过程中的时间复杂度不是线性的,在某些需要扩容的结点处,耗时会突增,尤其是元素越多,这个问题越明显
5、链表
增删改快
链表是一种物理存储单元上非连续、非顺序的存储结构,其物理结构不能只管的表示数据元素的逻辑顺序,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列的结点(链表中的每一个元素称为结点)组成,结点可以在运行时动态生成。
结点类实现:
public class Node<T> {
//存储元素
public T item;
//指向下一个结点
public Node next;
public Node(T item, Node next) {
this.item = item;
this.next = next;
}
}
生成链表:
public static void main(String[] args) throws Exception {
//构建结点
Node<Integer> first = new Node<Integer>(11, null);
Node<Integer> second = new Node<Integer>(13, null);
Node<Integer> third = new Node<Integer>(12, null);
Node<Integer> fourth = new Node<Integer>(8, null);
Node<Integer> fifth = new Node<Integer>(9, null);
//生成链表
first.next = second;
second.next = third;
third.next = fourth;
fourth.next = fifth;
}
6、单向链表
单向链表是链表的一种,它由多个结点组成,每个结点都由一个数据域和一个指针域组成,数据域用来存储数据,指针域用来指向其后继结点。链表的头结点的数据域不存储数据,指针域指向第一个真正存储数据的结点。
LinkList
package xianxing;
import java.util.Iterator;
public class LinkList<T> implements Iterable<T>{
//记录头结点
private Node<T> head;
//记录链表长度
private int N;
private class Node<T>{
T item;
Node<T> next;
public Node(T item, Node<T> next) {
this.item = item;
this.next = next;
}
}
public LinkList() {
head = new Node<T>(null, null);
N = 0;
}
//清空:断开头结点的连接,同时N=0
public void clear() {
head.next = null;
N = 0;
}
//获取链表的长度
public int length() {
return N;
}
//链表是否为空
public boolean isEmpty() {
return N==0;
}
//获取指定位置i的元素
public Object get(int i) {
Node<T> n = head.next;
for(int j=0; j<i; j++) {
n = n.next;
}
return n.item;
}
//插入元素
public void insert(T t) {
Node<T> n = head;
while(n.next != null) {
n = n.next;
}
Node<T> m = new Node<T>(t, null);
n.next = m;
N++;
}
//在指定位置i插入新的元素
public void insert(int i, T t) {
Node<T> n = head;
for(int j=0; j<i-1; j++) {
n = n.next;
}
Node<T> m = new Node<T>(t, n.next);
n.next = m;
N++;
}
//删除元素,并返回该元素
public Object remove(int i) {
Node<T> n = head;
for(int j=0; j<i-1; j++) {
n = n.next;
}
Node<T> m = n.next;
n = m.next;
N--;
return m.item;
}
//查找元素t第一次出现的位置
public int indexOf(T t) {
Node<T> 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 SIterator();
}
private class SIterator implements Iterator{
private Node n;
public SIterator() {
this.n = head;
}
@Override
public boolean hasNext() {
return n.next != null;
}
@Override
public Object next() {
n = n.next;
return n.item;
}
}
}
LinkTest
package xianxing;
//顺序表容器测试
public class LinkTest {
public static void main(String[] args) {
LinkList<String> sl = new LinkList<String>();
System.out.println("是否为空表"+sl.isEmpty());
sl.insert("李四");
sl.insert("王五");
sl.insert("李四");
sl.insert(0, "张三");
for(String s : sl) {
System.out.println(s);
}
System.out.println("第一个元素为:"+sl.get(0));
System.out.println("删除的元素为:"+sl.remove(0));
System.out.println("'李四'第一次出现的位置为:"+sl.indexOf("李四"));
}
}
7、双向链表
LinkedList
双向链表也叫双向表,是链表的一种,它由多个结点组成,每个结点都由一个数据域和两个指针域组成,数据域用来存储数据,其中一个指针域用来指向其后继结点,另一个指针域用来指向前驱结点。链表的头结点的数据域不存储数据,指向前驱结点的指针域值为null,指向后继结点的指针域指向第一个真正存储数据的结点。
TwoLinkList
package xianxing;
import java.util.Iterator;
public class TwoLinkList<T> implements Iterable<T>{
//记录头结点
private Node<T> head;
//记录链表长度
private int N;
//记录尾结点
private Node<T> last;
private class Node<T>{
T item;
//指向上一个结点
Node<T> pre;
Node<T> next;
public Node(T item, Node<T> next, Node<T> pre) {
this.item = item;
this.next = next;
this.pre = pre;
}
}
public TwoLinkList() {
this.head = new Node<T>(null, null, null);
this.last = null;
N = 0;
}
//清空:断开头结点的连接,同时N=0
public void clear() {
this.head.next = null;
this.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;
}
//获取指定位置i的元素
public Object get(int i) {
Node<T> n = head.next;
for(int j=0; j<i; j++) {
n = n.next;
}
return n.item;
}
//插入元素
public void insert(T t) {
if(isEmpty()) {
//如果为空
Node<T> newNode = new Node<T>(t, null, head);
//新结点成为尾结点
last = newNode;
//让头结点指向新结点
head.next = newNode;
}else {
Node<T> newNode = new Node<T>(t, null, last);
last.next = newNode;
last = newNode;
}
N++;
}
//在指定位置i插入新的元素
public void insert(int i, T t) {
Node<T> n = head;
for(int j=0; j<i-1; j++) {
n = n.next;
}
Node<T> m = new Node<T>(t, n.next,n);
n.next.pre = m;
n.next = m;
N++;
}
//删除元素,并返回该元素
public Object remove(int i) {
Node<T> n = head;
for(int j=0; j<i-1; j++) {
n = n.next;
}
Node<T> m = n.next;
n.next = m.next;
m.next.pre = n;
N--;
return m.item;
}
//查找元素t第一次出现的位置
public int indexOf(T t) {
Node<T> 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 SIterator();
}
private class SIterator implements Iterator{
private Node<T> n;
public SIterator() {
this.n = head;
}
@Override
public boolean hasNext() {
return n.next != null;
}
@Override
public Object next() {
n = n.next;
return n.item;
}
}
}
8、链表时间复杂度分析
get(int i):每一次查询,都需要从链表的头部开始,依次向后查找,随着数据元素N的增多,比较的元素越多,时间复杂度为O(n)
insert(int i,T t):每一次插入,需要先找到i位置的前一个元素,然后完成插入操作,随着数据元素N的增多,查找的元素越多,时间复杂度为O(n);
remove(int i):每一次移除,需要先找到i位置的前一个元素,然后完成插入操作,随着数据元素N的增多,查找的元素越多,时间复杂度为O(n)
相比较顺序表,链表的查询操作性能会比较低。因此,如果我们的程序中查询操作比较多,建议使用顺序表,增删操作比较多,建议使用链表。
9、单向链表反转
LinkList中添加
//对整个链表进行反转
public void reverse(){
if(isEmpty()){
return;
}
reverse(head.next);
}
//反转链表中某个结点,并把反转后的结点返回
public Node reverse(Node curr){
if(curr.next == null){
head.next = curr;
return curr;
}
Node pre = reverse(curr.next);
pre.next = curr;
curr.next = null;
return curr;
}
10、快慢指针
1、中间值问题
快慢指针指的是定义两个指针,这两个指针的移动速度一块一慢,以此来制造出自己想要的差值,这个差值可以让我们找到链表上相应的结点。一般情况下,快指针的移动步长为慢指针的两倍。
如下图,最开始,slow与fast指针都指向链表第一个节点,然后slow每次移动一个指针,fast每次移动两个指针。
package xianxing;
//测试类
public class Test {
public static void main(String[] args) throws Exception {
Node<String> first = new Node<String>("aa", null);
Node<String> second = new Node<String>("bb", null);
Node<String> third = new Node<String>("cc", null);
Node<String> fourth = new Node<String>("dd", null);
Node<String> fifth = new Node<String>("ee", null);
Node<String> six = new Node<String>("ff", null);
Node<String> seven = new Node<String>("gg", null);
//完成结点之间的指向
first.next = second;
second.next = third;
third.next = fourth;
fourth.next = fifth;
fifth.next = six;
six.next = seven;
//查找中间值
String mid = getMid(first);
System.out.println("中间值为:"+mid);
}
public static String getMid(Node<String> first) {
//定义两个指针
Node<String> fast = first;
Node<String> slow = first;
while(fast.next != null && fast != null){
fast = fast.next.next;
slow = slow.next;
}
return slow.item;
}
//结点类
private static class Node<T> {
//存储数据
T item;
//下一个结点
Node next;
private Node(T item, Node next) {
this.item = item;
this.next = next;
}
}
}
2、单向链表是否有环问题
问题:
方法:
package xianxing;
//有环问题
public class Test2 {
public static void main(String[] args) throws Exception {
Node<String> first = new Node<String>("aa",null);
Node<String> second = new Node<String>("bb", null);
Node<String> third = new Node<String>("cc", null);
Node<String> fourth = new Node<String>("dd", null);
Node<String> fifth = new Node<String>("ee", null);
Node<String> six = new Node<String>("ff", null);
Node<String> seven = new Node<String>("gg", null);
//完成结点之间的指向
first.next = second;
second.next = third;
third.next = fourth;
fourth.next = fifth;
fifth.next = six;
six.next = seven;
//产生环
seven.next = third;
//判断链表是否有环
boolean circle = isCircle(first);
System.out.println("first链表中是否有环:"+circle);
}
//判断链表中是否有环
public static boolean isCircle(Node<String> first) {
Node<String> fast = first;
Node<String> slow = first;
while(fast != null && fast.next != null){
slow = slow.next;
fast = fast.next.next;
if(fast.equals(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;
}
}
}
3、有环的入口问题
找到有环的入口
方法:定义三个指针,快、慢、临时指针,fast、slow每相遇一次,判断fast 或 slow有没有与 temp相遇,相遇就结束遍历,没有则temp向后移。
package xianxing;
public class Test3 {
public static void main(String[] args) throws Exception {
Node<String> first = new Node<String>("aa",null);
Node<String> second = new Node<String>("bb", null);
Node<String> third = new Node<String>("cc", null);
Node<String> fourth = new Node<String>("dd", null);
Node<String> fifth = new Node<String>("ee", null);
Node<String> six = new Node<String>("ff", null);
Node<String> seven = new Node<String>("gg", null);
//完成结点之间的指向
first.next = second;
second.next = third;
third.next = fourth;
fourth.next = fifth;
fifth.next = six;
six.next = seven;
//产生环
seven.next = third;
//查找环的入口结点
Node<String> entrance = getEntrance(first);
System.out.println("first链表中环的入口结点元素为:"+entrance.item);
}
//查找有环链表中环的入口结点
public static Node getEntrance(Node<String> first) {
Node<String> fast = first;
Node<String> slow = first;
Node<String> temp = first;
while(fast != null && fast.next != null){
slow = slow.next;
fast = fast.next.next;
if(temp.equals(fast) || temp.equals(slow)){
break;
}
if(fast.equals(slow)){
temp = temp.next;
}
}
return temp;
}
//结点类
private static class Node<T> {
//存储数据
T item;
//下一个结点
Node next;
public Node(T item, Node next) {
this.item = item;
this.next = next;
}
}
}
11、循环链表
循环链表,顾名思义,链表整体要形成一个圆环状。在单向链表中,最后一个节点的指针为null,不指向任何结点,因为没有下一个元素了。要实现循环链表,我们只需要让单向链表的最后一个节点的指针指向头结点即可。
package xianxing;
public class CycleList {
public static void main(String[] args) {
//构建结点
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;
}
private static class Node<T> {
//存储数据
T item;
//下一个结点
Node next;
public Node(T item, Node next) {
this.item = item;
this.next = next;
}
}
}
12、约瑟夫问题
41个人坐一圈,第一个人编号为1,第二个人编号为2,第n个人编号为n。
1.编号为1的人开始从1报数,依次向后,报数为3的那个人退出圈;
2.自退出那个人开始的下一个人再次从1开始报数,以此类推;
3.求出最后退出的那个人的编号。
思路:
1.构建含有41个结点的单向循环链表,分别存储1~41的值,分别代表这41个人;
2.使用计数器count,记录当前报数的值,pre记录上一个值;
3.遍历链表,每循环一次,count++;
4.判断count的值,如果是3,则从链表中删除这个结点并打印结点的值,把count重置为0;
package xianxing;
public class JosephTest {
public static void main(String[] args) {
Node<Integer> first = new Node<>(1,null);
Node<Integer> pre = first;
for(int i=2; i<=41; i++){
Node<Integer> newNode = new Node<>(i,null);
pre.next = newNode;
pre = newNode;
if(i == 41){
pre.next = first;
}
}
Node<Integer> n = first;
int count = 0;
while(n != n.next){
count++;
if(count == 3){
pre.next = n.next;
count = 0;
}
pre = n;
n = n.next;
}
System.out.println(n.item);
}
private static class Node<T> {
//存储数据
T item;
//下一个结点
Node next;
public Node(T item, Node next) {
this.item = item;
this.next = next;
}
}
}