本文概要:
顺序表的实现
链表的实现
链表反转问题
快慢指针方法
约瑟夫问题
线性表
线性表是最基本的、最简单的、也是最常用的一种数据结构。一个线性表是n个具有相同特征的数据元素的有限序列。
前驱元素:若A元素在B元素的前面,则称A元素为B元素的前驱元素
后继元素:若B元素在A元素的后面,则称B元素为A元素的后继元素
线性表的特征:数据元素之间具有一种“一对一”的关系。
第一个数据元素没有前驱,这个数据元素被称为头节点;
最后一个数据元素没有后继,这个数据元素被称为尾节点;
除第一个和最后一个数据元素之外,其他数据元素都有且仅有一个前驱和后继。
如果把线性表用数学语言来定义,则可以表示为(a1...ai-1,ai,ai+1,...an),ai-1领先于ai,ai领先于ai+1,称ai-1为ai的前驱,ai+1为ai的后继。
线性表的分类:
线性表中数据存储可以是顺序存储,也可以是链式存储,按照数据存储方式可以分为顺序表和链表。
1 顺序表
顺序表是在计算机内存中以数组的形式保存的线性表,线性表的顺序存储是指用一组地址连续的存储单元,依次存储线性表中各个元素,使得线性表在逻辑结构相邻的元素存储在相邻的物理存储单元中。在Java中ArrayList也是对顺序表的实现。
1.1 顺序表的实现
顺序表的API设计:
顺序表的代码实现:
/** * 顺序表的实现 * * @author liang chen * @version 1.0 * @className SequenceList * @date 2020/12/16 21:25 */public class SequenceList<T> implements Iterable<T> { /** * 定义一个数组来放数据元素 */ private T[] eles; /** * 定义一个变量表示顺序表的大小 */ private int size; /** * 构建函数,初始化顺序表的大小 * * @param capacity 容量 */ public SequenceList(int capacity) { this.eles = (T[]) new Object[capacity]; } /** * 清空顺序表 */ public void clear() { //先清除数组里面的元素,让gc回收 for (int i = 0; i < size; i++) { eles[i] = null; } this.size = 0; } /** * 判断顺序表是否为空 * * @return */ public boolean isEmpty() { return size > 0; } /** * 返回顺序表中元素个数 * * @return */ public int size() { return size; } /** * 返回指定位置的元素 * * @param index * @return */ public T get(int index) { rangeCheck(index); return eles[index]; } /** * 向顺序表添加一个元素 * * @param t 待插入元素 */ public void add(T t) { this.eles[size++] = t; } /** * 向顺序表指定位置添加一个元素 * * @param index 位置下标 * @param t 待插入元素 */ public void add(int index, T t) { rangeCheckForAdd(index); if (size - index >= 0) System.arraycopy(eles, index, eles, index + 1, size - index); eles[index] = t; size++; } /** * 移除指定位置的元素 * * @param index 位置下标 * @return */ public T remove(int index) { rangeCheck(index); T t = eles[index]; int numMoved = size - index - 1; if (numMoved > 0) System.arraycopy(eles, index + 1, eles, index, numMoved); eles[--size] = null; return t; } /** * 查找元素在顺序标中的位置,不在则返回-1 * * @param t 查找元素 * @return */ public int indexOf(T t) { for (int i = 0; i < size; i++) { if (t.equals(eles[i])) { return i; } } return -1; } /** * 索引检查 * * @param index */ private void rangeCheck(int index) { if (index >= size || index < 0) { throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); } } /** * 添加元素时索引检查 * * @param index */ private void rangeCheckForAdd(int index) { if (index > size || index < 0) { throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); } } private String outOfBoundsMsg(int index) { return "Index: " + index + ", Size: " + this.size; } /** * 实现迭代器方法。 * @return */ @Override public Iterator<T> iterator() { return new ListItr(); } private class ListItr implements Iterator { private int cursor; public ListItr() { this.cursor = 0; } @Override public boolean hasNext() { return cursor < size; } @Override public Object next() { return eles[cursor++]; } }}
1.2 顺序表的遍历
在Java中,遍历集合的方式一般都是用foreach循环,如果想让我们的SequenceList也支持foreach循环,需要做如下操作:
让SequenceList实现Iterable接口,重写iterator方法
在SequenceList提供一个内部类ListItr,实现Iterator接口,重写hasNext和next方法
public class SequenceList<T> implements Iterable<T> { /** * 实现迭代器方法。 * @return */ @Override public Iterator<T> iterator() { return new ListItr(); } private class ListItr implements Iterator { private int cursor; public ListItr() { this.cursor = 0; } @Override public boolean hasNext() { return cursor < size; } @Override public Object next() { return eles[cursor++]; } } }
1.3 顺序表的容量可变
上面实现出来的顺序表,在确定的容量范围之内操作是没有问题的,但是如果添加元素超过了初始化给的容量值,就会报错了,如下:
SequenceList<String> list = new SequenceList<>(3);list.add("11");list.add("22");list.add("33");list.add("44");
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 3
怎么样做到容量大小可变呢?在添加的时候如果发现容量不够就动态的添加容量,在删除元素时,当发现容量很大,但是里面元素很少,动态的缩小容量。
代码实现:
/** * 根据参数newSize重置eles大小 * @param newSize */ private void resize(int newSize){ //定义一个临时数组,指向原数组 T[] temp = eles; //创建新数组 eles = (T[]) new Object[newSize]; if (size >= 0) System.arraycopy(temp, 0, eles, 0, size); }
在添加方法和移除方法里面调用即可:
/** * 向顺序表添加一个元素 * * @param t 待插入元素 */public void add(T t) { if(size==eles.length){ resize(2*eles.length); } this.eles[size++] = t;}/** * 向顺序表指定位置添加一个元素 * * @param index 位置下标 * @param t 待插入元素 */public void add(int index, T t) { rangeCheckForAdd(index); if(size==eles.length){ resize(2*eles.length); } if (size - index >= 0) System.arraycopy(eles, index, eles, index + 1, size - index); eles[index] = t; size++;}/** * 移除指定位置的元素 * * @param index 位置下标 * @return */public T remove(int index) { rangeCheck(index); T t = eles[index]; int numMoved = size - index - 1; if (numMoved > 0) System.arraycopy(eles, index + 1, eles, index, numMoved); eles[--size] = null; if(size<=eles.length/4){ resize(eles.length/2); } return t;}
1.4 顺序表的时间复杂度
get(i):不难看出,不管元素数据N有多大,只需要一次eles[i]即可获取到对应的元素,所以时间复杂度为O(1);
insert(int i,T t):每次插入都需要移动i后面的元素,时间复杂度为O(n);
remove(int t):每次删除都需要移动i后面的元素,时间复杂度为O(n);
由于顺序表底层由数组实现,数组的长度是固定的,所以在操作过程中涉及到扩容,这会导致顺序表在使用过程中的时间复杂度不是线性的在某些需要扩容的节点处,耗时会突增,尤其是元素越多,这个问题越明显。
2 链表
顺序表的查询速度快,时间复杂度为O(1),但是增删的速度比较慢,因为每一次增删操作都伴随着大量的元素移动,这个问题有什么方案可以解决呢?有,我们可以使用另外一种存储结构实现线性表,链式存储结构。
链表:是一种在物理存储单元上非连续、非顺序的存储结构,其物理结构不能表示数据元素的逻辑顺序,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。
2.1 链表的实现
单向链表的实现:
public class LinkList<T> implements Iterable<T>{ /** * 定义变量表示链表大小 */ private int size; /** * 定义变量表示链表头节点 */ private Node<T> head; /** * 定义链表节点类 */ class Node<T>{ //数据元素 private T item; //下一个节点的地址 private Node<T> next; Node(T item,Node next){ this.item = item; this.next = next; } } /** * 构造函数,初始化头节点和size */ public LinkList() { this.head = new Node<>(null,null); this.size = 0; } /** * 清空顺序表 */ public void clear() { this.head = new Node<>(null,null); this.size = 0; } /** * 判断顺序表是否为空 * * @return */ public boolean isEmpty() { return size<=0; } /** * 返回顺序表中元素个数 * * @return */ public int size() { return size; } /** * 返回指定位置的元素 * * @param index * @return */ public T get(int index) { if(index<0 || index>=size){ throw new IllegalArgumentException("index 非法,index:"+index+",size:"+size); } Node<T> node = head; for (int i=0;i<=index;i++){ node = node.next; } return node.item; } /** * 向顺序表添加一个元素 * * @param t 待插入元素 */ public void add(T t) { //尾插法 //1、先找到尾部元素 Node<T> node = head; while (node.next!=null){ node = node.next; } //2、插入元素 node.next = new Node<>(t,null); this.size++; } /** * 向顺序表指定位置添加一个元素 * * @param index 位置下标 * @param t 待插入元素 */ public void add(int index, T t) { if(index< 0 || index>size+1){ throw new IllegalArgumentException("index 非法,index:"+index+",size:"+size); } Node<T> node = head; for (int i = 0;i < index;i++){ node = node.next; } node.next = new Node<>(t,node.next); size++; } /** * 移除指定位置的元素 * * @param index 位置下标 * @return */ public T remove(int index) { if(index<0 || index>=size){ throw new IllegalArgumentException("index 非法,index:"+index+",size:"+size); } Node<T> preNode = node(index-1); Node<T> curNode = preNode.next; T element = curNode.item; preNode.next = curNode.next; size--; return element; } /** * 返回当前index的node * @param index * @return */ private Node<T> node(int index){ Node<T> node = head.next; for (int i = 0;i < index;i++){ node = node.next; } return node; } /** * 查找元素在顺序标中的位置,不在则返回-1 * * @param t 查找元素 * @return */ public int indexOf(T t) { Node<T> node = head; for (int i = 0;i < size;i++){ node = node.next; if (t.equals(node.item)){ return i; } } return -1; } /** * 实现迭代器方法。 * @return */ @Override public Iterator<T> iterator() { return new ListItr(); } private class ListItr implements Iterator<T> { private Node<T> node; public ListItr() { this.node = head; } @Override public boolean hasNext() { return node.next!=null; } @Override public T next() { node = node.next; return node.item; } }}
双向链表的实现:
/** * 双向链表的实现 * @author liang chen * @version 1.0 * @className TowWayLinkList * @date 2020/12/26 21:58 */public class TowWayLinkList<T> implements Iterable<T>{ private Node<T> head; private Node<T> last; private int size; @Override public Iterator<T> iterator() { return new ListItr(); } class ListItr implements Iterator<T>{ private Node<T> first; public ListItr(){ this.first = head; } @Override public boolean hasNext() { return first.next!=null; } @Override public T next() { first = first.next; return first.item; } } /** * 定义双向链表的节点类 * @param */ class Node<T>{ /** * 数据元素 */ private T item; /** * 数据前驱 */ private Node<T> pre; /** * 数据后继 */ private Node<T> next; /** * 构造函数 * @param item 数据元素 * @param pre 前驱 * @param next 后继 */ public Node(T item, Node<T> pre,Node<T> next){ this.item = item; this.pre = pre; this.next = next; } } public TowWayLinkList(){ this.head = new Node<>(null,null,null); this.last = null; this.size = 0; } public void clear(){ this.head.next = null; this.last = null; this.size = 0; } public int size(){ return size; } /** * * @param t */ public void add(T t){ addLast(t); } /** * 在index处添加元素 * @param index 索引 * @param t 数据元素 */ public void add(int index,T t){ if(index<0 || index>size){ throw new IllegalArgumentException("index 非法,index:"+index+",size:"+size); } if(index == size){ addLast(t); }else { linkBefore(t,node(index)); } } /** * 移除index处的节点 * @param index 索引 * @return */ public T remove(int index){ if(index<0 || index >=size){ throw new IllegalArgumentException("index 非法,index:"+index+",size:"+size); } //获取删除位置的元素 Node<T> curr = node(index); T element = curr.item; //获取删除位置元素的前驱 Node<T> pre = curr.pre; //获取删除位置的后继 Node<T> next = curr.next; //将删除位置元素的前驱的后继指向删除位置的后继 pre.next = next; //判断是不是删除最后一个元素 if(next == null){ last = pre; }else { next.pre = pre; } size--; return element; } /** * 获取index处的数据元素 * @param index 索引 * @return */ public T get(int index){ if(index<0 || index >=size){ throw new IllegalArgumentException("index 非法,index:"+index+",size:"+size); } Node<T> curr = node(index); return curr.item; } /** * 查找元素在链表中第一次出现的位置 * @param t 数据元素 * @return */ public int indexOf(T t){ Node<T> node = head; for (int i = 0; i < size-1; i++) { node = node.next; if(node.item.equals(t)){ return i; } } return -1; } /** * 判断是否为空链表 * @return */ public boolean isEmpty(){ return size == 0; } /** * 找到index处的节点 * @param index 索引 * @return */ private Node<T> node(int index) { //index小于size的一半从前往后找,否则从后往前找 Node<T> node; if(index < (size>>1)){ node = head.next; for (int i = 0; i < index; i++) node = node.next; }else { node = last; for (int i = size-1; i > index; i--) node = node.pre; } return node; } /** * 想当前元素前面添加元素 * @param t 数据元素 * @param node 当前节点 */ private void linkBefore(T t,Node<T> node){ Node<T> pre = node.pre; Node<T> newNode = new Node<>(t,pre,node); node.pre = newNode; if(pre==null){ head = newNode; }else { pre.next = newNode; } size++; } /** * 头插 * @param t 数据元素 */ public void addFirst(T t){ Node<T> next = head.next; Node<T> newNode = new Node<>(t,head,next); head.next = newNode; if(last == null){ last = newNode; } size++; } /** * 尾插 * @param t 数据元素 */ public void addLast(T t){ final Node<T> l = last; Node<T> newNode = new Node<>(t,l,null); last = newNode; //添加第一个元素时,last为空 if(l == null){ head.next = newNode; last.pre = head; }else { l.next = newNode; } size++; }}
2.2 链表的遍历
同样可以实现Iterable接口,实现iterator方法来提供高级for循环的遍历方式。
2.3 链表的时间复杂度
get(int index):每次查询都需要从头部或者尾部查询,时间复杂度为O(n),在上面双向链表的实现中,做了一定的优化,没有每次查询都是从头部查询,而是看index所处的位置靠前还是靠后,如果index=size/2则从后往前找。
add(int index,T t):需要找到index位置的元素的前驱,然后完成插入操作,时间复杂度为O(n)
remove(int index):每次移除都需要找到index位置的前驱,然后完成移除操作,时间复杂度为O(n)
相比顺序表,链表插入和删除的时间复杂度虽然一样,但仍然有它的优势,因为链表的物理地址不要求是连续的,它不需要预先指定存储空间大小,不涉及扩容操作。
相比顺序表,链表的查询操作性能比较低,因此如果我们的程序中查询比较多,建议使用顺序表,增删比较多,建议使用链表。
2.4 链表反转
单链表反转,面试高频题
需求:
原链表:1->2->3->4
反转后:4->3->2->1
思路:
使用递归的思想,从原链表的第一个数据节点开始,一次递归调用反转每一个节点,直到把最后一个节点反转完毕,整个链表就反转完毕了。
/** * 反转整个链表 */public void reverse(){ //判断是否为空链表 if(isEmpty()){ return; } reverse(head.next);}/** * 反转指定节点curr,并把反转后的节点返回 * @param curr * @return */private Node<T> reverse(Node<T> curr){ if(curr.next == null){ head.next = curr; return curr; } //递归反转当前节点的下一个节点,返回链表反转后,当前节点的上一个节点 Node<T> pre = reverse(curr.next); //让返回的节点的下一个节点变为当前节点 pre.next = curr; //把当前节点的下一个节点变为null curr.next = null; return curr;}
2.5 快慢指针
快慢指针是指定义两个指针,这两个指针的移动速度一快一慢,以此来制造出想要的差值。一般情况下,快指针的移动步长为慢指针的两倍。
2.5.1 中间值问题
看如下代码,然后完成需求
/** * 1、取中间值 */@Testpublic void getMiddleTest(){ 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);}/** * 节点类 * @param */private class Node<T>{ private T item; private Node<T> next; public Node(T item,Node<T> next){ this.item = item; this.next = next; }}
请实现getMid的方法,找到链表的中间值。
private String getMid(Node<String> first) { //定义快慢两个指针,并初始化为first Node<String> fast = first; Node<String> low = first; while (fast!=null && fast.next!=null){ low = low.next; fast = fast.next.next; } return low.item;}
执行结果:
中间值为:dd
上面@Test
注解这里使用到了junit包,pom.xml如下:
<properties> <maven.compiler.source>1.8maven.compiler.source> <maven.compiler.target>1.8maven.compiler.target>properties><dependencies> <dependency> <groupId>junitgroupId> <artifactId>junitartifactId> <version>4.13version> <scope>compilescope> dependency>dependencies>
2.5.2 单向链表是否有环
看如下代码,然后完成需求,看到下面这段代码的同学不妨自己先尝试实现一下。
/** * 2、判断单向链表是否有环 */@Testpublic void isCircleTest(){ 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; //实现isCircle方法,判断链表是否有环 boolean result = isCircle(first); System.out.println("first链表中是否有环:"+result);}
isCircle的实现如下:
private boolean isCircle(Node<String> first) { //定义快慢两个指针,并初始化为first Node<String> fast = first; Node<String> slow = first; //使用两个指针遍历链表,如果fast和slow相遇则说明有环,否则无环 while (fast!=null && fast.next!=null){ slow = slow.next; fast = fast.next.next; if(fast.equals(slow)){ return true; } } return false;}
2.5.3 有环链表入口问题
有这样一个结论:使用快慢指针遍历链表,当快慢指针相遇表明有环,此时使用另一个指针temp从头节点开始以步长为1(和慢指针相同步长)遍历链表,temp和慢指针相遇的节点就是该环的入口。
/** * 3、有环链表入口问题 */@Testpublic void getEntranceTest(){ 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 = six; //查找环的入口,请实现该方法 Node<String> entrance = getEntrance(first); System.out.println("有环链表first的入口节点元素是:"+entrance.item);}
getEntrance方法的实现:
/** * 查找环的入口 * 定律: * 使用快慢指针遍历链表,当快慢指针相遇表明有环,此时使用另一个指针temp从头节点开始 * 以步长为1(和慢指针相同步长)遍历链表,temp和慢指针相遇的节点就是该环的入口。 * @param first 链表 * @return */private Node<String> getEntrance(Node<String> first) { //定义快慢指针和临时指针 Node<String> fast = first; Node<String> slow = first; Node<String> temp = null; while (fast!=null && fast.next!=null){ slow = slow.next; fast = fast.next.next; //判断快慢指针是否相遇,需要判断temp是否为null, // 只有temp为null时才需要比较快慢指针所指向的节点 if(temp==null) { if (slow.equals(fast)) { temp = first; } }else { temp = temp.next; //当slow和temp相遇时,相遇点就是环的入口 if (temp.equals(slow)){ break; } } } return temp;}
2.6 循环链表
循环链表,顾名思义,链表整体形成一个圆环状,在单链表中最后一个节点的指针为null,而循环链表则是最后一个节点的指针指向第一个节点。
循环链表的构造:
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 = first;
2.7 约瑟夫问题
问题描述:
传说有这样一个故事,在罗马人占领桥塔帕特后,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 JosephTest { /** * 解决约瑟夫问题测试 */ @Test public void josephTest() { //1、构建循环链表,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> node = first; //记录当前节点的上一个节点 Node<Integer> before = null; while (node != node.next) { //模拟报数 count++; //判断是不是3 if (count == 3) { //如果是3,重置计数器count,并删除当前节点,当前节点后移 before.next = node.next; //输出当前节点元素 System.out.print(node.item + ","); count = 0; } else { //如果不是3,before变为当前节点,当前节点后移 before = node; } node = node.next; } //输出最后一个节点元素 System.out.println(node.item); } /** * 节点类 * * @param */ private class Node<T> { private T item; private Node<T> next; public Node(T item, Node<T> 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
好了,本次内容就是这些,学无止境,关注我,我们一起学习进步。如果觉得内容还可以,帮忙点个赞,点个在看呗,谢谢~我们下期见。
下期预告:栈、队列的实现和相关算法