一、线性表
线性表是n个具有相同特征的数据元素的有限序列,它是一种在实际中广泛使用的数据结构,常见的线性表有:顺序表、链表、栈、队列......
如图:顺序表与链表的区别看了标题的伙伴就应该知道,我们这次要讲解的主角是顺序表,所以链表等下次再来介绍了!
二、实现顺序表
顺序表是用一段物理地址连续的存储单元依次存储数据元素的数据结构,一般情况下采用数组存储,不过,它在数组上完成了数据的增删查改!
1、实现过程中的各种文件
如图:为了是实现这个顺序表,我创建了一个arrayList包,这个包底下有三个文件:
IList接口、MyArrayList类、Test类。
ILIst接口:接口中包含实现数据增删查改的功能,当然,接口中的方法不需要具体实现!
MyArrayList类:这个类定义了数组这个成员变量,并且具体实现了接口中的每一种功能!
Test类:这个类主要是来使用这个数据结构的,里面包含main方法!
2、MyArrayList类:
MyArrayList 全部代码:
package arrayList;
import java.util.Arrays;
//具体实现接口中的功能
public class MyArrayList implements IList {
//定义成员变量
public int[] elem;
public int useSize;
//创建构造方法
public MyArrayList(){
this.elem=new int[10];
this.useSize=0;
}
//在数组末尾添加数据
public void add(int data){
//判断数组是否满了?
//未满:将数据存入数组elem[useSize]处,同时useSize++
//满了:先扩容,再存放数据
if(isFull()){
//拷贝数组
elem= Arrays.copyOf(elem,2*elem.length);
}
this.elem[useSize]=data;
useSize++;
}
//判断数组是否满了
public boolean isFull(){
return useSize==elem.length;
}
//在指定位置添加数据
public void add(int data,int pos){
//判断pos的合理性
try{
checkPos(pos);
}catch (PosNotLegalException e){
e.printStackTrace();
}
//判断数组是否满了
if(isFull()){
elem=Arrays.copyOf(elem,2*elem.length);
}
for (int i = useSize-1; i <=pos ; i++) {
elem[i+1]=elem[i];
}
elem[pos]=data;
useSize++;
}
//判断pos是否合法?
private void checkPos(int pos){
if(pos<0||pos>useSize){
throw new PosNotLegalException("pos不合法!");
}
}
//判断是否包含某个元素
public boolean contains(int toFind){
for (int i = 0; i < useSize; i++) {
if(elem[i]==toFind){
return true;
}
}
return false;
}
//查找某个元素的位置
public int indexOf(int toFind){
for (int i = 0; i < useSize; i++) {
if(elem[i]==toFind){
return i;
}
}
return -1;
}
//获取pos位置的值
public int get(int pos){
try{
checkPosGetAndSet(pos);
}catch (PosNotLegalException e){
e.printStackTrace();
}
return elem[pos];
}
//判断get 的pos是否合法?
private void checkPosGetAndSet(int pos) {
if(pos<0||pos>=useSize){
throw new PosNotLegalException("pos不合法!");
}
}
//打印顺序表:
public void display(){
for (int i = 0; i < useSize; i++) {
System.out.print(elem[i]+" ");
}
System.out.println();
}
//将pos位置的值设置为value
public void set(int value,int pos){
try{
checkPosGetAndSet(pos);
}catch (PosNotLegalException e){
e.printStackTrace();
}
elem[pos]=value;
}
//去除指定元素
public void toRemove(int toRemove){
//如果找不到元素,返回
int pos=indexOf(toRemove);
if(pos==-1){
return;
}
//如果找到,去除元素
for (int i=pos;i<useSize-1;i++){
elem[i]=elem[i+1];
}
useSize--;
}
//去除数组内容
public void clear(){
useSize=0;
}
}
在数组末尾增加数据:
在数组末尾增加数据的功能很简单:
它的逻辑是,先使用isFull方法判断数组空间是否满了?如果未满:将数据添加到elem[useSize]处,同时useSize++;如果满了,就先給数组扩容,再添加数据到数组末尾!
具体实现:
//在MyArrlist类中:
//在数组末尾添加数据
public void add(int data){
//判断数组是否满了?
//未满:将数据存入数组elem[useSize]处,同时useSize++
//满了:先扩容,再存放数据
if(isFull()){
//拷贝数组
elem= Arrays.copyOf(elem,2*elem.length);
}
this.elem[useSize]=data;
useSize++;
}
//判断数组是否满了
public boolean isFull(){
return useSize==elem.length;
}
在指定位置插入数据:
在指定位置添加数据也不难,它的逻辑如下:只要我们将 从数组末尾到pos位置 的元素都往后挪动一个位置,到最后它就会将数组下标为pos的位置空出来,这样我们就可以添加我们需要添加的元素了!
但是在此之前,我们需要判断pos的合法性:
1、pos不能小于0,因为数组的下标最小为0
2、pos不能大于当前的useSize
还有一种情况:当数组空间满了的情况下,要先给数组扩容
具体实现:
这里我实现了一个功能,如果输入的pos不合法:抛出异常!(异常的知识可以在我的其他文章中寻找)
那么,我们就需要另外创建一个异常类:
//在MyArrayList类中:
//在指定位置添加数据
public void add(int data,int pos){
//判断pos的合理性:如果不合理,抛出异常!
try{
checkPos(pos);
}catch (PosNotLegalException e){
e.printStackTrace();
}
//判断数组是否满了
if(isFull()){
elem=Arrays.copyOf(elem,2*elem.length);
}
for (int i = useSize-1; i <=pos ; i++) {
elem[i+1]=elem[i];
}
elem[pos]=data;
useSize++;
}
//判断pos是否合法?
private void checkPos(int pos){
if(pos<0||pos>useSize){
throw new PosNotLegalException("pos不合法!");
}
这里给大家看看异常的效果:如果输入的pos不合法:
判断是否包含某个元素:
这个方法的主要逻辑是:通过循环遍历数组,如果找到要查找的元素,返回true,否则返回false
具体实现:
//在MyArrayList类中
//判断是否包含某个元素
public boolean contains(int toFind){
for (int i = 0; i < useSize; i++) {
if(elem[i]==toFind){
return true;
}
}
return false;
}
查找某个元素的位置:
这个方法的主要逻辑是:通过循环遍历数组,如果找到要查找的元素,返回其下标,否则返回一个负数
具体实现:
//在MyArrayList类中:
//查找某个元素的位置
public int indexOf(int toFind){
for (int i = 0; i < useSize; i++) {
if(elem[i]==toFind){
return i;
}
}
return -1;
}
获取pos位置的值:
这个方法的逻辑很简单:先判断传入的pos是否合法?不合法则抛出异常!合法则返回该下标的数组元素的值!
具体实现:
//在MyArrayList类中:
//获取pos位置的值
public int get(int pos){
try{
checkPosGetAndSet(pos);
}catch (PosNotLegalException e){
e.printStackTrace();
}
return elem[pos];
}
//判断get 的pos是否合法?
private void checkPosGetAndSet(int pos) {
if(pos<0||pos>=useSize){
throw new PosNotLegalException("pos不合法!");
}
}
打印顺序表:
具体实现:
//在MyArrayList类中:
//打印顺序表:
public void display(){
for (int i = 0; i < useSize; i++) {
System.out.print(elem[i]+" ");
}
System.out.println();
}
将pos位置的值设置为value:
具体实现:
//在MyArrayList类中:
//将pos位置的值设置为value
public void set(int value,int pos){
try{
checkPosGetAndSet(pos);
}catch (PosNotLegalException e){
e.printStackTrace();
}
elem[pos]=value;
}
去除pos位置的元素:
该方法的逻辑如下:
1、先看看能不能找到你要查找的元素,如果不能找到,return
如果可以找到,去除元素
2、去除元素原理:
从pos位置的元素开始,依次使当前元素的值等于下一个元素的值,然后i加1,直到i<useSIze
具体实现:
//去除指定元素
public void toRemove(int toRemove){
//如果找不到元素,返回
int pos=indexOf(toRemove);
if(pos==-1){
return;
}
//如果找到,去除元素
for (int i=pos;i<useSize-1;i++){
elem[i]=elem[i+1];
}
useSize--;
}
去除数组:
具体实现:
//去除数组内容
public void clear(){
useSize=0;
}
3、IList接口:
package arrayList;
//在这个接口实现数据的增删查改操作
public interface IList {
//在数组末尾增加数据
void add(int data);
boolean isFull();
//在指定位置添加元素
void add(int data,int pos);
//判断是否包含某个元素
boolean contains(int toFind);
//查找某个元素的位置
int indexOf(int toFind);
//获取pos位置的值
int get(int pos);
//打印顺序表:
void display();
//将pos位置的值设置为value
void set(int value,int pos);
//去除指定元素
void toRemove(int toRemove);
//去除数组内容
void clear();
}
三、ArrayList类
其实,前面讲解顺序表的实现都是为了现在做铺垫,只是为了让我们更好地理解顺序表的原理!
在实际使用过程中,我们并不需要总是自己搭建一个顺序表,因为Java当中,人家已经实现好了对应的顺序表,也就是我们接下来要讲解的:ArrayList类!
1、实例化ArrayLIst类:
在实例化ArrayList类之前,我们得来看看这个类的源码,如图可知:
ArrayList类是一个泛型,它继承了另外一个泛型类,因此,在实例化的时候,我们要给ArrayLIst类指定类型。
实例化两种方式:
//实例化ArrayList类的两种写法:
List<Integer> list=new ArrayList<>();
ArrayList<Integer> arrayList=new ArrayList<>();
那两种方式有什么区别?
2、ArrayList类的构造方法
如果我们要认识ArrayLIst这个类,首先我们要去了解这个类的构造方法!
三种构造方法:
ArrayList() //无参构造
ArrayList(Collection<? extends E> c) //利用其他Collection构建ArrayList
ArrayList(int initialCapacity) //指定顺序表初始容量
ArrayList()
>让我们先看看这个构造方法的源码:
通过前面学习我们知道,顺序表其实就是一个数组!那么这个ArrayList类其实也一样,它内部也有一个数组:也就是上图中的elementData。
>>如下图:它就是ArrayList类内部定义的数组,可以知道,这个数组现在还没有分配内存,它此时只是一个数据引用!
那么在调用这个构造方法的时候,它会使这个数组赋值为
DEFAULTCAPACITY_EMPTY_ELEMENTDATA
>>>那么这个东西是什么?让我们来看看:
DEFAULTCAPACITY_EMPTY_ELEMENTDATA这个东西原来是一个常量数组,但是它也没有分配内存!所以数组最终的长度为0,那么这个数组还可以增添元素吗?
>>>> 让我来试试看看行不行?
程序运行:竟然可以!
为什么一个数组长度为0的数组可以添加元素?
答:数组在add方法中进行了扩容!
ArrayList(int initialCapacity)
>同样,我们先来看看这个构造方法的源码:
可以看到:这个构造方法主要由if—else分支语句构成,主要划分为三种情况。
1、initialCapacity大于0,给数组分配指定的内存
2、initialCapacity=0,情况与第一个构造方法一样,此时的数组是一个空数组
3、initialCapacity<0,抛出异常
ArrayList(Collection<? extends E> c)
>先来看看该构造方法的源码:
>>这个构造方法比较难理解,这是因为首先它传入的参数也不太好理解!那我们来研究一下它可以传入什么参数!
其实我们可以将创建的arrayList作为参数出传入这个构造方法:
如下图:
原因是:1、Collection是ArrayList类的顶层接口,因此ArrayList类型的对象有资格作为参数
2、arrayList这个对象指定的类型和arrayList2这个对象指定的类型一致,都为Integer
3、ArrayList类中的一些方法:
public E remove(int index)
删除指定下标的元素:
程序运行:
public boolean remove(Object o)
删除数组中的指定值(最多一个)
程序运行:
public List<E> subList(int fromIndex, int toIndex)
>截取指定位置的元素,返回值用一个接口的对象去接收!
程序运行:
>>那我们来看一个问题:
这里我两次打印了arrayList,第一次打印的时候是在未使用subList这个方法之前
第二次打印是在使用该方法后,并且将list数组中的第一个元素修改为99
如图:为什么修改了list中数组的第一个元素,arrayList中的数组的13被修改为99?
这是因为:截取后并没有产生一个新的对象,而是将list直接引用了1下标这个位置!所以修改了list中数组的下标为0元素,也就修改了arrayList中数组的下标为1的元素
4、ArrayList的遍历
ArrayList可以使用三种方式遍历:for循环+下标、foreach、使用迭代器
for循环+下标:
public class Test {
public static void main(String[] args) {
//给数组添加元素
ArrayList<Integer> arrayList=new ArrayList<>();
arrayList.add(12);
arrayList.add(13);
arrayList.add(14);
arrayList.add(15);
arrayList.add(16);
//使用for循环打印数组
for(int i=0;i<arrayList.size();i++){
System.out.print(arrayList.get(i)+" ");
}
}
}
程序运行:
foreach :
public class Test {
public static void main(String[] args) {
//给数组添加元素
ArrayList<Integer> arrayList=new ArrayList<>();
arrayList.add(12);
arrayList.add(13);
arrayList.add(14);
arrayList.add(15);
arrayList.add(16);
//使用foreach打印数组
for(Integer x:arrayList){
System.out.print(x+" ");
}
}
}
程序运行:
使用迭代器 :
public class Test {
public static void main(String[] args) {
//给数组添加元素
ArrayList<Integer> arrayList=new ArrayList<>();
arrayList.add(12);
arrayList.add(13);
arrayList.add(14);
arrayList.add(15);
arrayList.add(16);
//使用迭代器
Iterator<Integer> it=arrayList.iterator();
while(it.hasNext()){
System.out.print(it.next()+" ");
}
}
}
程序运行:
四、ArrayList的使用
讲解完ArrayList这个类,让我们使用它来实现一些功能
1、简单的洗牌算法:
是这样的,我们想要实现一个功能,帮助我们洗牌,然后轮流给三个人发牌,每个人发五张牌,最后打印出来每个人的牌!(这里就不考虑卡牌中的大小王!而将卡牌定义为四种图案,分别为红桃,梅花,方块,黑桃,每种图案有卡牌值为1到13)
各种文件如图:
Card类:
抽象出来一个卡牌类,每张卡牌都有其花色和卡牌值
package demo;
public class Card {
//定义成员变量花色和卡牌值
public String suit;//花色
public int rank;//卡牌值
//实现带有参数的构造方法
public Card(String suit, int rank) {
this.suit = suit;
this.rank = rank;
}
//生成toString方法,方便后续打印
@Override
public String toString() {
return "{"+suit+","+rank+"}";
}
}
Cards类:
这里还需要抽象出来一个卡牌组类,指一整套卡牌的集合 ,同时这个类还包含创建卡牌组、洗牌、发牌等各种功能:
package demo;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
public class Cards {
//定义四种花色
public final String[] suits={"♥️","♣","♦","♠"};
//创建卡牌组
public List<Card> buyCard(){
//创建一个数组cardList,这个数组即可表示为卡牌组
List<Card> cardList=new ArrayList<>();
//实例化每张卡牌
for (int i = 0; i <4 ; i++) {
for (int j = 1; j <=13 ; j++) {
Card card=new Card(suits[i],j);
cardList.add(card);
}
}
return cardList;
}
//洗牌
public void shuffle(List<Card> cardList){
Random random=new Random();
for (int i = cardList.size()-1; i >0; i--) {
int randIndex= random.nextInt(i);
swap(cardList,i,randIndex);
}
}
//交换卡牌
public void swap(List<Card> list,int i,int j){
Card tmp=list.get(i);
list.set(i,list.get(j));
list.set(j,tmp);
}
//发牌
public void drawCard(List<Card> cardList){
//将每个人收到的卡牌放到数组中
List <Card> hand1=new ArrayList<>();
List <Card> hand2=new ArrayList<>();
List <Card> hand3=new ArrayList<>();
//创建二维数组,数组的每个元素为一个一维数组
List <List<Card>> hands=new ArrayList<>();
hands.add(hand1);
hands.add(hand2);
hands.add(hand3);
//轮流发牌
for(int i=0;i<5;i++){
for(int j=0;j<3;j++){
Card card=cardList.remove(0);
hands.get(j).add(card);
}
}
//打印牌
System.out.println("第一个人的牌:"+hand1);
System.out.println("第二个人的牌:"+hand2);
System.out.println("第三个人的牌:"+hand3);
}
}
Test类:
这个类用来测试我们的功能:
package demo;
import java.util.ArrayList;
import java.util.List;
public class Test {
public static void main(String[] args) {
//创建卡牌组,用cardList来接收,打印牌
Cards cards=new Cards();
List<Card> cardlist=cards.buyCard();
System.out.println(cardlist);
//洗牌,然后打印牌
System.out.println("洗牌之后");
cards.shuffle(cardlist);
System.out.println(cardlist);
//抓牌,然后打印每个人抓到的牌
System.out.println("抓牌之后");
cards.drawCard(cardlist);
}
}
程序运行:
2、杨辉三角数
杨辉三角数示例图如下:我们要运用ArrayList类的知识点实现形成一个杨辉三角数!
1
1 1
1 2 1
1 3 3 1
1 4 6 4 1
………………………………
分析得知,可以将杨辉三角数看成一个二维数组:
具体实现:
public static List<List<Integer>> triangleNum(int x){
//创建一个二维数组
List <List<Integer>> list=new ArrayList<>();
//因为第一行之前不存在[i-1]行,因此第一行数组必须自己赋值!!!
List<Integer> curRow1=new ArrayList<>();
list.add(curRow1);
curRow1.add(1);
//给数组的每一个元素赋值
for (int i = 1; i <x; i++) {
//i表示每一行,每进入一行,实例化一个一维数组
List<Integer> curRow=new ArrayList<>();
list.add(curRow);
//数组的第一个元素为1
curRow.add(1);
//数组中间元素[i][j]=[i-1][j]+[i-1][j-1]
for (int j = 1; j <i ; j++) {
List <Integer> preRow=list.get(i-1);
int m=preRow.get(j-1);
int n=preRow.get(j);
curRow.add(m+n);
}
//数组的最后一个元素为1
curRow.add(1);
}
return list;
}
3、字符串问题:
str1:welcome to bit
str2:come
要求:删除第一个字符串当中所有的第二个字符串当中的字符,如该例子,最终结果为:wl t bit
问题分析:遍历str1数组,拿到某个字符,判断该字符在不在str2,如果不在,就放在ArrayList当中
public static void func (String str1,String str2){
List<Character> list=new ArrayList<>();
for (int i = 0; i < str1.length(); i++) {
char ch=str1.charAt(i);
if(!str2.contains(ch+"")){
list.add(ch);
}
}
for(char x:list){
System.out.print(x);
}
}
又或者,不借助ArrayList,使用字符串拼接也可以完成!
public static void func (String str1,String str2){
StringBuilder str=new StringBuilder();
for (int i = 0; i < str1.length(); i++) {
char ch=str1.charAt(i);
if(!str2.contains(ch+"")){
str.append(ch);
}
}
System.out.println(str);
}