目录
一.线性表
二、顺序表
2.1 简单模拟顺序表的实现
三、ArrayList简介
3.1 ArrayList的扩容机制(附源码分析)
四、使用示例:
4.1扑克牌
4.2.杨辉三角
线性表(linear list)是n个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使用的数据结构,常见的线性表:顺序表、链表、栈、队列...
线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的,线性表在物理上存储时,通常以数组和链式结构的形式存储。
顺序表是用一段物理地址连续(逻辑上也连续)的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数据的增删查改。
import java.util.ArrayList;
import java.util.Arrays;
public class MyArrayList {
public int elem[];
//记录当前有多少元素存入数组之中
public int usedSize;
public int Default_Capacity = 10;
// 利用构造方法更好的完成对数组的初始化
public MyArrayList(){
this.elem = new int[Default_Capacity];
}
// 打印顺序表
public void display() {
for (int i = 0; i < this.usedSize ; i++) {
System.out.print(elem[i]+" ");
}
System.out.println();
}
// 新增元素,默认在数组最后新增
public void add(int data) {
//在新增之前需要判断数组是否满
if(isFull(elem)){
this.elem = Arrays.copyOf(this.elem,2*elem.length);
}
elem[usedSize] = data;
usedSize++;
}
public boolean isFull(int elem[]){
return usedSize == elem.length;
}
private void checkAddPos(int pos){
if(pos < 0 || pos > usedSize){
throw new PosIndexNotLegalException("pos下标不合法");
}
}
// 在 pos 位置新增元素
public void add(int pos, int data) {
//在新增元素之前,需要检查pos位置是否合法以及数组是否满
try {
checkAddPos(pos);
if (isFull(elem)){
elem = Arrays.copyOf(elem,2*elem.length);
}
for (int i = usedSize-1; i >= pos ; i--) {
elem[i+1] = elem[i];
}
elem[pos] = data;
usedSize++;
}catch (PosIndexNotLegalException e){
e.printStackTrace();
}
}
// 判定是否包含某个元素
public boolean contains(int toFind) {
for (int i = 0; i < usedSize; i++) {
if (toFind == elem[i]){
return true;
}
}
return false;
}
// 查找某个元素对应的位置
public int indexOf(int toFind) {
for (int i = 0; i < usedSize ; i++) {
if (toFind == elem[i]){
return i;
}
}
return -1;
}
public void checkPos(int pos){
if(pos < 0 || pos >=usedSize ){
throw new PosIndexNotLegalException("下标不合法");
}
}
// 获取 pos 位置的元素
public int get(int pos) {
//同样在获取pos下标之前,需要判断下标是否合法
checkPos(pos);
return elem[pos];
}
// 给 pos 位置的元素设为 value
public void set(int pos, int value) {
checkPos(pos);
elem[pos] = value;
}
//删除第一次出现的关键字key
public void remove(int toRemove) {
int index = indexOf(toRemove);
if(index == -1){
System.out.println("没有你要删除的数字");
return;
}
for (int i = index; i < usedSize-1; i++) {
elem[i] = elem[i+1];
}
usedSize--;
}
// 获取顺序表长度
public int size() {
return usedSize;
}
// 清空顺序表
public void clear() {
usedSize = 0;
}
}
在集合框架中,ArrayList是一个普通的类,实现了List接口,具体框架图如下:
示例:
下面我们通过查看源码的方式来探究一下ArrayList的扩容机制:
我们先来看看ArrayList无参的构造方法:
再来看看elementData和DEFAULTCAPACITY_EMPTY_ELEMENTDATA是什么:
可以得知:在调用ArrayList构造方法(无参)创建对象时,此时给数组分配的空间大小为0.
✨✨
小插曲:或许有人会对EMPTY_ELEMENTDATA 和DEFAULTCAPACITY_EMPTY_ELEMENTDATA两个的存在感到疑惑,以下将通过原码来解答这一问题:
分析:虽然这两个Object类的数组都被static final修饰,但是通过观察我们发现,两者的主要功能是区分,区分调用了哪个构造方法,调用无参的构造方法则是使用DEFAULTCAPACITY_EMPTY_ELEMENTDATA,而当调用有参的构造方法时,当传入的值为0时,就使用EMPTY_ELEMENTDATA。
✨✨
回到上一步,我们通过源码发现在默认容量其实为10
但是为什么显示的是0呢?下面我们通过查看add方法来进一步观察:
因为是第一次实例化对象(调用无参构造方法),所以Size为0,即1作为参数(minCapacity)传入ensureCapacityInternal中:
在ensureCapacityInternal中先调用了CalculateCapacity方法:
我们发现:if(.....)中的语句与无参的构造方法中的语句一致
于是返回DEFAULT_CAPACITY(10)和minCapacity(1)中的最大值,即返回10.
而10又作为ensureExplicitCapacity的参数,我们再来观察ensureExplicitCapacity的源码:
minCapacity为10,而前面我们得知,elementData为空(长度为0),于是条件成立,执行grow方法:
分析: minCapacity为10,oldCapacity为0,而newCapacity = 0 + (0/2)= 0.
第一个if(...)语句中 newCapacity-minCapacity = 0 -10 < 0.
条件成立,于是newCapacity被赋值为10,
第二个if(...)语句中是让newCapacity-MAX_ARRAY_SIZE(可分配数组最大的大小),而MAX_ARRAY_SIZE为:
显然条件不成立。
于是elemData进行扩容(为10),这里也印证了为什么之前源码中DEFAULT_CAPACITY为10的原因。
因此我们可以得出结论,当创建一个ArrayLIst(调用无参构造)类时,被分配的数组大小为0,而使用add方法时,数组大小被扩容为10.
那大家一定很好奇,当10个元素都被放满的时候,该怎么处理?
这时add方法中的Size为10,minCapacity为11。
这时if(...)条件即不成立,calculateCapacity返回11
minCapacity - elementData.length = 11- 10 > 0
进入grow方法:
oldCapacity为10,newCapacity为10+10>>1 = 15.
于是我们得出结论:扩容为1.5倍的扩容
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
class Card{
public int rank;
public String suit;
public Card(int rank,String suit){
this.rank = rank;
this.suit = suit;
}
@Override
public String toString() {
return " "+suit+" "+rank;
}
}
public class CardDemo {
public static final String[] suit = {"♥","♣","♦","♠"};
public List buyDeskCard() {
List cards = new ArrayList<>();
for (int i = 1; i <= 13 ; i++) {
for (int j = 0; j < 4; j++) {
Card card = new Card(i,suit[j]);
cards.add(card);
}
}
return cards;
}
public void shuffle(List cards) {
for (int i = cards.size()-1; i > 0 ; i--) {
Random random = new Random();
int index = random.nextInt(i);
swap(cards,index,i);
}
}
public void swap(List cards,int i,int j){
Card tmp = cards.get(i);
cards.set(i,cards.get(j));
cards.set(j,tmp);
}
public List> deal(List cards){
List hand1 = new ArrayList<>();
List hand2 = new ArrayList<>();
List hand3 = new ArrayList<>();
List> 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 = cards.remove(0);
hands.get(j).add(card);
}
}
return hands;
}
}
测试代码:
import java.util.List;
public class Test2 {
public static void main(String[] args) {
CardDemo cardDemo = new CardDemo();
List ret = cardDemo.buyDeskCard();
cardDemo.shuffle(ret);
System.out.println(ret);
System.out.println("发牌");
List> ret2 = cardDemo.deal(ret);
for (int i = 0; i < ret2.size(); i++) {
System.out.println("第"+i+"个人的牌: "+ret2.get(i));
}
}
}
运行结果:
class Solution {
public List> generate(int numRows) {
//利用二维数组的思维解题,先创建两个顺序表
List> ret = new ArrayList<>();
List list = new ArrayList<>();
//先将第一层构造完成
list.add(1);
ret.add(list);
//构造后面层
for(int i = 1;i < numRows;i++){
List curRow = new ArrayList<>();//当前层
curRow.add(1);
List prevRow = ret.get(i-1);//前一层
for(int j = 1;j < i;j++){
int num = prevRow.get(j)+prevRow.get(j-1);
curRow.add(num);
}
//在每层的其余元素构造完之后(除每行第一个和最后一个元素),将最后一个元素填入。
curRow.add(1);
ret.add(curRow);
}
return ret;
}
}
该示例来自力扣——杨辉三角。
总结:
当然顺序表也有诸多不是很好的地方:
1. 顺序表中间/头部的插入删除,时间复杂度为O(N)
2. 增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗。
3. 增容一般是呈2倍的增长,势必会有一定的空间浪费。例如当前容量为1000,满了以后增容到2000,我们再继续插入了5个数据,后面没有数据插入了,那么就浪费了995个数据空间。
因此我们引入链表的概念来解决这类问题。