🍓 简介:java系列技术分享(👉持续更新中…🔥)
🍓 初衷:一起学习、一起进步、坚持不懈
🍓 如果文章内容有误与您的想法不一致,欢迎大家在评论区指正🙏
🍓 希望这篇文章对你有所帮助,欢迎点赞 👍 收藏 ⭐留言 📝
线性表是最基本、也是最常用的一种数据结构。一个线性表是n个具有相同特性的数据元素的有限序列。
前驱元素:若A元素在B元素的前面,则称A为B的前驱元素
后继元素:若B元素在A元素的后面,则称B为A的后继元素
线性表的特征: 数据元素之间具有一种“一对一”的逻辑关系。
线性表的分类:
线性表中数据存储的方式可以是顺序存储,也可以是链式存储,按照数据的存储方式不同,可以把线性表分为顺序表和链表。
顺序表是一种
线性数据结构
,其中的元素按照一定的顺序存储在内存中,可以通过下标来访问每个元素。顺序表中的元素存储在一块连续的内存空间中,因此可以通过指针来访问每个元素。顺序表是一种简单的数据结构,常用于实现一些基本的算法,例如排序、查找等。
注意点:
第一点
需要向外部提供遍历的方式,在java中,遍历集合的方式一般都是用的是foreach循环,如果想让我们的SequenceList也能支持foreach循环
实现Iterable接口,重写iterator方法;
提供一个内部类SIterator,实现Iterator接口,重写hasNext方法和next方法
;第二点
创建对象时就需要指定容器的大小,初始化指定大小的数组来存储元素,当我们插入元素时,如果已经插入了5个元素,还要继续插入数据,则会报错,就不能插入了。这种设计不符合容器的设计理念,因此我们在设计顺序表时,应该考虑它的容量的伸缩性。
添加元素
时,应该检查当前数组的大小是否能容纳新的元素,如果不能容纳,则需要创建新的容量更大的数组,我们这里创建一个是原数组两倍容量的新数组存储元素。移除元素时
应该检查当前数组的大小是否太大,比如正在用100个容量的数组存储10个元素,这样就会造成内存空间的浪费,应该创建一个容量更小的数组存储元素。如果我们发现数据元素的数量不足数组容量的1/4,则创建一个是原数组容量的1/2的新数组存储元素。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;
}
//判断当前线性表是否为空表
public boolean isEmpty() {
return N == 0;
}
//获取线性表的长度
public int length() {
return N;
}
//获取指定位置的元素
public T get(int i) {
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 (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) {
//记录索引i处的值
T current = eles[i];
//索引i后面元素依次向前移动一位即可
for (int index = i; index < N - 1; index++) {
eles[index] = eles[index + 1];
}
//元素个数-1
N--;
if (N < eles.length / 4) {
resize(eles.length / 2);
}
return current;
}
//查找t元素第一次出现的位置
public int indexOf(T t) {
for (int i = 0; i < N; i++) {
if (eles[i].equals(t)) {
return i;
}
}
return -1;
}
//根据参数newSize,重置eles的大小
public void resize(int newSize) {
//定义一个临时数组,指向原数组
T[] temp = eles;
//创建新数组
eles = (T[]) new Object[newSize];
//把原数组的数据拷贝到新数组即可
for (int i = 0; i < N; i++) {
eles[i] = temp[i];
}
}
@Override
public Iterator iterator() {
return new SIterator();
}
private class SIterator implements Iterator{
private int cur;
public SIterator(){
this.cur=0;
}
@Override
public boolean hasNext() {
return cur<N;
}
@Override
public T next() {
return eles[cur++];
}
}
}
public class SequenceListTest {
public static void main(String[] args) {
//创建顺序表对象
SequenceList<String> sl = new SequenceList<>(5);
//测试插入
sl.insert("篮球");
sl.insert("乒乓球");
sl.insert("羽毛球");
sl.insert(1, "排球");
sl.insert("保龄球");
sl.insert("拳击");
for (String s : sl) {
System.out.println(s);
}
System.out.println("------------------------------------------");
//测试获取
String getResult = sl.get(1);
System.out.println("获取索引1处的结果为:" + getResult);
//测试删除
String removeResult = sl.remove(0);
System.out.println("删除的元素是:" + removeResult);
//测试清空
sl.clear();
System.out.println("清空后的线性表中的元素个数为:" + sl.length());
}
}
顺序表的时间复杂度取决于具体的操作。对于顺序表中的基本操作,例如插入、删除、查找等,其时间复杂度为O(1)
。但是,如果需要在顺序表中进行大量的插入或删除操作,可能会导致整个顺序表的元素重新排列
,此时时间复杂度将会变为O(n)。
java中ArrayList集合的底层也是一种顺序表,使用数组实现
,同样提供了增删改查以及扩容等功能。
如果想看详细的顺序表实现,可查看ArrayList源码
发现虽然顺序表的查询很快,时间复杂度为O(1),但是增删的效率是比较低的,因为每一次增删操作都伴随着大量的数据元素移动。我们可以使用另外一种存储结构实现线性表,链式存储结构。
链表是一种物理存储单元上非连续、非顺序的存储结构
,其物理结构不能只管的表示数据元素的逻辑顺序,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列的结点(链表中的每一个元素称为结点)组成,结点可以在运行时动态生成。
单向链表是链表的一种,它由多个结点组成,每个结点都由一个数据域和一个指针域组成,数据域用来存储数据,指针域用来指向其后继结点。链表的头结点的数据域不存储数据,指针域指向第一个真正存储数据的结点。
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位置的结点
//原来i位置的前一个节点指向新结点即可
pre.next= new Node(t, curr);
//元素的个数+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;
}
}
public class LinkListTest {
public static void main(String[] args) {
//创建顺序表对象
LinkList<String> sl = new LinkList<>();
//测试插入
sl.insert("篮球");
sl.insert("乒乓球");
sl.insert("羽毛球");
sl.insert(1, "排球");
sl.insert("保龄球");
sl.insert("拳击");
for (String s : sl) {
System.out.println(s);
}
System.out.println("------------------------------------------");
//测试获取
String getResult = sl.get(1);
System.out.println("获取索引1处的结果为:"+getResult);
//测试删除
String removeResult = sl.remove(0);
System.out.println("删除的元素是:"+removeResult);
//测试清空
sl.clear();
System.out.println("清空后的线性表中的元素个数为:"+sl.length());
}
}
双向链表也叫双向表,是链表的一种,它由多个结点组成,每个结点都由一个数据域和两个指针域组成,数据域用来存储数据,其中一个指针域用来指向其后继结点,另一个指针域用来指向前驱结点。链表的头结点的数据域不存储数据,指向前驱结点的指针域值为null,指向后继结点的指针域指向第一个真正存储数据的结点。
public class TowWayLinkList<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 TowWayLinkList() {
//初始化头结点和尾结点
this.head = new Node(null,null,null);
this.last=null;
//初始化元素个数
this.N=0;
}
//清空链表
public void clear(){
this.head.next=null;
this.head.pre=null;
this.head.item=null;
this.last=null;
this.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()){
//如果链表为空:
//创建新的结点
Node newNode = new Node(t,head, null);
//让新结点称为尾结点
last=newNode;
//让头结点指向尾结点
head.next=last;
}else {
//如果链表不为空
Node oldLast = last;
//创建新的结点
Node newNode = new Node(t, oldLast, null);
//让当前的尾结点指向新结点
oldLast.next=newNode;
//让新结点称为尾结点
last = newNode;
}
//元素个数+1
N++;
}
//向指定位置i处插入元素t
public void insert(int i,T t){
//找到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);
//让i位置的前一个结点的下一个结点变为新结点
pre.next=newNode;
//让i位置的前一个结点变为新结点
curr.pre=newNode;
//元素个数+1
N++;
}
//获取指定位置i处的元素
public T get(int i){
Node n = head.next;
for(int index=0;index<i;index++){
n=n.next;
}
return n.item;
}
//找到元素t在链表中第一次出现的位置
public int indexOf(T t){
Node n = head;
for(int i=0;n.next!=null;i++){
n=n.next;
if (n.next.equals(t)){
return i;
}
}
return -1;
}
//删除位置i处的元素,并返回该元素
public T remove(int i){
//找到i位置的前一个结点
Node pre = head;
for(int index=0;index<i;index++){
pre=pre.next;
}
//找到i位置的结点
Node curr = pre.next;
//找到i位置的下一个结点
Node nextNode= curr.next;
//让i位置的前一个结点的下一个结点变为i位置的下一个结点
pre.next=nextNode;
//让i位置的下一个结点的上一个结点变为i位置的前一个结点
nextNode.pre=pre;
//元素的个数-1
N--;
return curr.item;
}
@Override
public Iterator<T> iterator() {
return new TIterator();
}
private class TIterator implements Iterator{
private Node n;
public TIterator(){
this.n=head;
}
@Override
public boolean hasNext() {
return n.next!=null;
}
@Override
public Object next() {
n=n.next;
return n.item;
}
}
}
public class TowWayLinkListTest {
public static void main(String[] args) {
//创建双向链表对象
TowWayLinkList<String> sl = new TowWayLinkList<>();
//测试插入
sl.insert("篮球");
sl.insert("乒乓球");
sl.insert("羽毛球");
sl.insert(1, "排球");
sl.insert("保龄球");
sl.insert("拳击");
for (String s : sl) {
System.out.println(s);
}
System.out.println("--------------------------------------");
System.out.println("第一个元素是:"+sl.getFirst());
System.out.println("最后一个元素是:"+sl.getLast());
System.out.println("------------------------------------------");
//测试获取
String getResult = sl.get(1);
System.out.println("获取索引1处的结果为:"+getResult);
//测试删除
String removeResult = sl.remove(0);
System.out.println("删除的元素是:"+removeResult);
//测试清空
sl.clear();
System.out.println("清空后的线性表中的元素个数为:"+sl.length());
}
}
java中LinkedList
集合也是使用双向链表实现
,并提供了增删改查等相关方法
相比较顺序表,链表插入和删除的时间复杂度虽然一样,但仍然有很大的优势,因为链表的物理地址是不连续的,它不需要预先指定存储空间大小,或者在存储过程中涉及到扩容等操作,同时它并没有涉及的元素的交换。
相比较顺序表,链表的查询操作性能会比较低。因此,如果我们的程序中查询操作比较多,建议使用顺序表,增删操作比较多,建议使用链表。
问题描述
传说有这样一个故事,在罗马人占领乔塔帕特后,39 个犹太人与约瑟夫及他的朋友躲到一个洞中,39个犹太人决定宁愿死也不要被敌人抓到,于是决定了一个自杀方式,41个人排成一个圆圈,第一个人从1开始报数,依次往后,如果有人报数到3,那么这个人就必须自杀,然后再由他的下一个人重新从1开始报数,直到所有人都自杀身亡为止。然而约瑟夫和他的朋友并不想遵从。于是,约瑟夫要他的朋友先假装遵从,他将朋友与自己安排在第16个与第31个位置,从而逃过了这场死亡游戏 。
问题转换:
现在有41个人坐一圈,第一个人编号为1,第二个人编号为2,第n个人编号为n。
编号为1的人开始从1报数,依次向后,报数为3的那个人退出圈;
自退出那个人开始的下一个人再次从1开始报数,以此类推;
求出最后退出的那个人的编号。
代码实现
public class JosephTest {
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;
//如果是最后一个结点,那么需要让最后一个结点的下一个结点变为first,变为循环链表了
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++;
//判断当前报数是不是为3
if (count == 3) {
//如果是3,则把当前结点删除调用,打印当前结点,重置count=0,让当前结点n后移
before.next = n.next;
System.out.print(n.item + ",");
count = 0;
n = n.next;
} else {
//如果不是3,让before变为当前结点,让当前结点后移;
before = n;
n = n.next;
}
}
//打印最后一个元素
System.out.println();
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;
}
}
}
main方法运行结果
我们把生活中的栈的概念引入到计算机中,就是供数据休息的地方,它是一种数据结构,数据既可以进入到栈中,又可以从栈中出去。
栈是一种基于先进后出(FILO)
的数据结构,是一种只能在一端进行插入和删除操作的特殊线性表。它按照先进后出的原则存储数据,先进入的数据被压入栈底,最后的数据在栈顶,需要读数据的时候从栈顶开始弹出数据(最后一个数据被第一个读出来)。
我们称数据进入到栈的动作为压栈
,数据从栈中出去的动作为弹栈
。
public class Stack<T> implements Iterable<T> {
//记录首结点
private Node head;
//栈中元素的个数
private int N;
private class Node {
public T item;
public Node next;
public Node(T item, Node next) {
this.item = item;
this.next = next;
}
}
/**
* 初始化首节点不存储元素,不指向任何节点
*/
public Stack() {
this.head = new Node(null, null);
this.N = 0;
}
//判断当前栈中元素个数是否为0
public boolean isEmpty() {
return N == 0;
}
//获取栈中元素的个数
public int size() {
return N;
}
//把t元素压入栈
public void push(T t) {
//找到首结点指向的第一个结点
Node oldFirst = head.next;
//创建新结点
Node newNode = new Node(t, null);
//让首结点指向新结点
head.next = newNode;
//让新结点指向原来的第一个结点
newNode.next = oldFirst;
//元素个数+1;
N++;
}
//弹出栈顶元素
public T pop() {
//找到首结点指向的第一个结点
Node oldFirst = head.next;
//如果栈中没有元素则直接返回
if (oldFirst == null) {
return null;
}
//让首结点指向原来第一个结点的下一个结点
head.next = oldFirst.next;
//元素个数-1;
N--;
return oldFirst.item;
}
@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;
}
}
}
public class StackTest {
public static void main(String[] args) {
//创建栈对象
Stack<String> stack = new Stack<>();
//测试压栈
stack.push("a");
stack.push("b");
stack.push("c");
stack.push("d");
System.out.println("因为栈是FILO先进后出,所以栈顶的元素先出站");
for (String item : stack) {
System.out.println(item);
}
System.out.println("------------------------------");
//测试弹栈
String result = stack.pop();
System.out.println("弹出的元素是:"+result);
System.out.println("剩余的元素个数:"+stack.size());
}
}
问题描述:
给定一个字符串,里边可能包含"()"小括号和其他字符,请编写程序检查该字符串的中的小括号是否 成对 出现。
例如:
"(上海)(长安)":正确匹配
"上海((长安))":正确匹配
"上海(长安(北京)(深圳)南京)":正确匹配
"上海(长安))":错误匹配
"((上海)长安":错误匹配
思路
代码实现
其中Stack
是上面实现的类
public class BracketsMatchTest {
public static void main(String[] args) {
String str = "上海(长安)())";
boolean match = isMatch(str);
System.out.println(str+"中的括号是否匹配:"+match);
String str2 = "上海(长安)()";
boolean match2 = isMatch(str2);
System.out.println(str2+"中的括号是否匹配:"+match2);
}
/**
* 判断str中的括号是否匹配
* @param str 括号组成的字符串
* @return 如果匹配,返回true,如果不匹配,返回false
*/
public static boolean isMatch(String str){
//1.创建栈对象,用来存储左括号
Stack<String> chars = new Stack<>();
//2.从左往右遍历字符串
for (int i = 0; i < str.length(); i++) {
String currChar = str.charAt(i)+ "";
//3.判断当前字符是否为左括号,如果是,则把字符放入到栈中
if (currChar.equals("(")){
chars.push(currChar);
}else if(currChar.equals(")")){
//4.继续判断当前字符是否是有括号,如果是,则从栈中弹出一个左括号,并判断弹出的结果是否为null,如果为null证明没有匹配的左括号,如果不为null,则证明有匹配的左括号
String pop = chars.pop();
if (pop==null){
return false;
}
}
}
//5.判断栈中还有没有剩余的左括号,如果有,则证明括号不匹配
if (chars.size()==0){
return true;
}else{
return false;
}
}
}
返回成功,匹配的为true,不匹配为false
队列是一种基于
先进先出(FIFO)
的数据结构,是一种只能在一端进行插入,在另一端进行删除操作的特殊线性表,它按照先进先出的原则存储数据,先进入的数据,在读取数据时先读被读出来。
public class Queue<T> implements Iterable<T> {
//记录首结点
private Node head;
//记录最后一个结点
private Node last;
//记录队列中元素的个数
private int N;
private class Node {
public T item;
public Node next;
public Node(T item, Node next) {
this.item = item;
this.next = next;
}
}
public Queue() {
this.head = new Node(null, null);
this.last = null;
this.N = 0;
}
//判断队列是否为空
public boolean isEmpty() {
return N == 0;
}
//返回队列中元素的个数
public int size() {
return N;
}
//向队列中插入元素t
public void enqueue(T t) {
if (last == null) {
//当前尾结点last为null
last = new Node(t, null);
head.next = last;
} else {
//当前尾结点last不为null
Node oldLast = last;
last = new Node(t, null);
oldLast.next = last;
}
//元素个数+1
N++;
}
//从队列中拿出一个元素
public T dequeue() {
if (isEmpty()) {
return null;
}
Node oldFirst = head.next;
head.next = oldFirst.next;
N--;
//因为出队列其实是在删除元素,因此如果队列中的元素被删除完了,需要重置
if (isEmpty()) {
last = null;
}
return oldFirst.item;
}
@Override
public Iterator<T> iterator() {
return new QIterator();
}
private class QIterator implements Iterator {
private Node n;
public QIterator() {
this.n = head;
}
@Override
public boolean hasNext() {
return n.next != null;
}
@Override
public Object next() {
n = n.next;
return n.item;
}
}
}
public class QueueTest {
public static void main(String[] args) {
//创建队列对象
Queue<String> q = new Queue<>();
//测试队列的enqueue方法
q.enqueue("a");
q.enqueue("b");
q.enqueue("c");
q.enqueue("d");
for (String str : q) {
System.out.println(str);
}
System.out.println("-------------------------------");
//测试队列的dequeue方法
String result = q.dequeue();
System.out.println("出队列的元素是:"+result);
System.out.println("剩余的元素个数:"+q.size());
}
}