前言
最近花了接近一个月的时间,期间加上上课考试等,断断续续的,不过今天为止,总算是把我以前--望--闻--生--畏--的数据结构全部梳理了一遍,同时用一个工程将他们全部记录了下来,下面是这个工程的目录截图,可能会在后续陆续加上一些常用的操作,比如两个栈实现一个队列,两个队列实现一个栈等等,但是还是步步为营,慢慢来,先从基础开始。
由于我目前数据结构水平不高,主要是将基础打牢,所以我并没有去做一些高级用法,主要工作是弄懂常用数据结构的实现原理,我采用的办法就是自己纯手动去一个一个实现,如果遇到实再不会的就借助网上资料,看别人的思路,然后自己再实现一遍,虽然方法比较笨,也比较费时,并且要完美的实现一个数据结构要考虑的问题是非常多的,所以我最后实现的都是基本操作,即便如此,但是我觉得还是很值得的,至少在现在看来,我对数据结构的认识比之前提高了很多,我甚至有这样一种感受,之前我很讨厌的数据结构,现在我居然好像很喜欢,甚至有点痴迷,因为里面很多东西是非常巧妙的,你甚至有时候会发出这样的感叹-----哇,真的神奇!!!
同时,为了防止遗忘,因为这个过程也是比较长的,所以对自己来说也是个复习吧,再次将我梳理的这个工程,从线性表开始一个一个整理出来,加深映像,好了,废话不多说,先从线性表开始!!
目录
1.线性表相关概念
2.线性表的基本操作
3.线性表的优缺点
4.线性表的使用场景
5.线性表的完整代码
正文
1.线性表相关概念
为了查漏补缺,在说明数组,先来了解一下 线性表 这个东西,线性表是最常用且最简单的一种数据结构,简言之,一个线性表是n个数据元素的有限序列。线性表根据表示方式不同,分为顺序表示和链式表示两种,其中顺序表示方式(数组)就是今天的主角啦,另外一种链式表示方式的线性表就是下一篇要梳理的--链表。
下面是顺序表示的线性表的概念:
用一组地址连续的存储单元依次存储线性表的数据元素的数据结构。
好了,虽然说概念在实际中很少涉及到,但是有时候掌握概念让你在实际使用的时候会知道如何取舍,以及,如果你找工作的话,万一人家面试官问到了呢,对不,嘿嘿!
为了方便表述,下文中的线性表都是指的顺序表示的线性表。
2.线性表的基本操作
a.线性表的初始化
首先搞清楚,初始化线性表需要用什么来初始化,那么概念的作用来了,地址连续,自然而然的我们想到了->数组!
线性表初始化的工作可以分为三种
1.默认情况初始化
使用默认情况时,将使用默认设定的容量来初始化数组的大小,同时默认的数据元素为默认的0(如果申明为int类型数组的话)
2.初始化含有一个数据元素的线性表
使用一个数据元素来初始化时,数组的容量使用默认容量,与第一种情况不同的是,将数组的第一个元素,也就是0号元素,设置为该数据元素。
3.初始化一个数据元素,并且容量为给定值的线性表
与第二种情况的区别是,在声明数组并指定容量的时候,使用给定的容量值来初始化。
初始化代码如下:
// 无参构造线性表
public SquenceList() {
this.element = new Object[DEFAULT_SIZE];// 初始化列表的空间
this.capacity = DEFAULT_SIZE;// 实际分配数组长度
}
// 初始化含有一个元素的线性表
public SquenceList(int elem) {
this();// 调用空参数构造函数
this.element[size++] = elem;
}
// 指定长度并初始化一个元素创建线性表
public SquenceList(int elem, int size) {
this.capacity = 1;// 初始化
// 扩充数组空间使得capicity的size且是2的n次方
while (this.capacity < size) {
this.capacity <<= 1;//相当于*2
}
this.element = new Object[this.capacity];
this.element[size++] = elem;
}
b.获取线性表的长度
由于我们用数组实现的,一种可行的办法是按顺序从0号索引处开始逐个比较,并记录长度值,直到碰到索引值为0时为止,这样我们就获取到了长度值,但是这样有个弊端就是无法添加值为0的元素,而且效率比较低。
另外一种可行的办法是,我们可以为线性表对象声明一个成员变量 size,然后在每次添加元素或者删除元素的时候,更新这个size值就可以了,然后要获取长度的时候,直接返回这个size即可,这样就省去了每次获取长度的时候都要遍历一遍数组。
// 获取线性表的索引为i处的元素(i介于0~size-1)
public int getelem(int i) {
if (i < 0 || i > size - 1) {// 检测是否越界
throw new IndexOutOfBoundsException("线性表的索引越界:" + i);
}
return (int) this.element[i];
}
c.获取指定值的索引
这个从索引0处开始,循环遍历一遍数组,遇到索引对应的值和给定值相等的时候,就返回对应的索引即可,如果找完了仍然没有找到,那么就返回-1,用来代表未找到。
// 查找元素在线性表中的索引
public int findindex(int elem) {
for (int i = 0; i < size; i++) {
if (this.element[i].equals(elem))
return i;// 找到返回对应的索引
}
return -1;// 若没有找到返回-1
}
d.获取指定索引处的值
这个就很简单了,拿到了索引,直接返回索引对应的值即可。
但是仅仅做这一步工作的话,就又有问题了,作为一个标准的程序员,写代码最基本的就是--健壮性,所以还需要判断一下索引是否越界,也就是索引值是否大于线性表的容量,以及索引小于0的情况。
// 获取线性表的索引为i处的元素(i介于0~size-1)
public int getelem(int i) {
if (i < 0 || i > size - 1) {// 检测是否越界
throw new IndexOutOfBoundsException("线性表的索引越界:" + i);
}
return (int) this.element[i];
}
e.插入元素
插入相对复杂点,因为对于数组来说,涉及到元素的移动。
考虑到程序的健壮性,在插入之前还要做的一件事就是判断是否需要扩容,因为有可能当前数组已经满了,那么就需要扩大数组的容量。如果需要扩容的话,建议是每次扩大为之前容量的两倍。具体的扩大容量的方法在下面会讲到。
然后在指定索引处插入对应的值,分两步,一、插入元素第i个位置空出来,从i位置开始所有元素后移一个位置,这里可以借助Java提供的原生方法System.arraycopy来快速实现;二、指定i索引处的值为给定值。
当然最后不要忘了将记录数组长度的变量size加一。
也可以加一些额外的特殊需求的方法,比如在线性表末尾插入元素,这个就是直接调用一下上面写好的方法即可,只不过默认的插入索引为数组长度代表的索引。
// 插入一个元素到线性表的第i个索引处
public void insert(int elem, int i) {
// 是否需要扩充容量
this.ensureCapicty(size + 1);
// 插入元素第i个位置空出来从i位置开始所有元素后移一个位置
System.arraycopy(this.element, i, this.element, i + 1, size - i);
// 将元素插入到指定位置
this.element[i] = elem;
// 当前容量增加1
this.size++;
}
// 在线性表末尾插入元素
public void add(int elem) {
this.insert(elem, this.size);
}
f.删除元素
仍然考虑到程序的健壮性,首先判断需要判断的是删除元素的索引是否是合法的,然后获取删除索引处的值,保存用于最后返回,看到这你可能会很奇怪,我删除元素就直接做删除元素的操作就可以了嘛,为啥还要给这个删除元素的方法弄个返回值呢,其实我刚开始也有这样的困惑,但是你仔细想想,在开发调试的时候,删除了一个元素,如何知道你删除的元素是不是你想要删除的那个元素呢,如果我不给这个方法添加返回值的话,那么是不是还要额外重新写代码,或者通过其它方式来判断呢?所以多思考,参考优秀的人的优秀思想是很有好处的,嘿嘿,好了,不扯远了。
删除一个元素仍然分为两步,一、获取需要前移的元素个数,如果大于0的话,也就是有元素需要前移,那么使用System.arraycopy方法进行相关元素的前移即可;二、清空最后一个元素,也就是将size-1处的值置空。
最后,别忘了返回一下开头获取到的值哦。
可添加的相关额外方法有:删除数组最后一个元素,removeLast,只需要调用一下上面封装好的方法即可。
// 删除线性表中第i个元素并返回该处的值
public int delete(int i) {
if (i < 0 || i > size - 1) {// 检测删除位置对不对
throw new IndexOutOfBoundsException("删除位置索引越界:" + i);
}
// 获得i处的元素值
int del = (int) this.element[i];
// 删除元素后从i+1位置开始元素要前移
int moved = this.size - i - 1;// 需要移动元素的个数
if (moved > 0) {
System.arraycopy(this.element, i + 1, this.element, i, moved);
}
// 清空最后一个元素
this.element[--size] = null;
return del;
}
// 移除线性表中最后一个元素
public int remove() {
return this.delete(size - 1);
}
g.容量扩充
容量扩充,较好的扩充是每次扩充为之前两倍,同时也要考虑到扩大了两倍仍然不够的情况,所以最好使用while循环来控制,直到容量足够为止,然后将原来的元素拷贝到新的位置上。
// 扩充线性表的容量
private void ensureCapicty(int currentCapicty) {
if (currentCapicty > this.capacity) {// 若实际的所需容量大于实际的容量则扩充使得实际容量大于所需容量且是2的次方
while (this.capacity < currentCapicty) {
this.capacity <<= 1;
}
// 将原来的元素拷贝到新的位置上
this.element = Arrays.copyOf(this.element, this.capacity);
}
}
h.判断线性表是否为空
这个也很简单啦,一句代码size==0即可。
i.清空线性表
清空线性表分两步:一、置size为0;二、置线性表所有的元素为空。
// 清空线性表
public void clear() {
// 将所有元素赋值为null
Arrays.fill(this.element, null);
this.size = 0;
}
3.线性表的优缺点
优点:查找快(知道索引的情况下,只需O(1)的时间即可定位并获取到值)
缺点:插入删除效率低下,因为顺序存储的原因,插入删除就会导致元素的不断移动。
4.线性表的使用场景
当需要的操作大多数情况下为查找操作时,那么就可以采用线性表。
5.线性表的完整代码
package SquenceList;
import java.util.Arrays;
public class SquenceList {
private int DEFAULT_SIZE = 4;// 线性表的默认长度空间
private int capacity;// 线性表的实际分配数组长度
private int size = 0;// 线性表的当前元素个数及线性表的长度
private Object[] element;// 数据元素封装一个数组
// 无参构造线性表
public SquenceList() {
this.element = new Object[DEFAULT_SIZE];// 初始化列表的空间
this.capacity = DEFAULT_SIZE;// 实际分配数组长度
}
// 初始化含有一个元素的线性表
public SquenceList(int elem) {
this();// 调用空参数构造函数
this.element[size++] = elem;
}
// 指定长度并初始化一个元素创建线性表
public SquenceList(int elem, int size) {
this.capacity = 1;// 初始化
// 扩充数组空间使得capicity的size且是2的n次方
while (this.capacity < size) {
this.capacity <<= 1;//相当于*2
}
this.element = new Object[this.capacity];
this.element[size++] = elem;
}
// 获得线性表的长度
public int length() {
return this.size;
}
// 获取线性表的索引为i处的元素(i介于0~size-1)
public int getelem(int i) {
if (i < 0 || i > size - 1) {// 检测是否越界
throw new IndexOutOfBoundsException("线性表的索引越界:" + i);
}
return (int) this.element[i];
}
// 查找元素在线性表中的索引
public int findindex(int elem) {
for (int i = 0; i < size; i++) {
if (this.element[i].equals(elem))
return i;// 找到返回对应的索引
}
return -1;// 若没有找到返回-1
}
// 扩充线性表的容量
private void ensureCapicty(int currentCapicty) {
if (currentCapicty > this.capacity) {// 若实际的所需容量大于实际的容量则扩充使得实际容量大于所需容量且是2的次方
while (this.capacity < currentCapicty) {
this.capacity <<= 1;
}
// 将原来的元素拷贝到新的位置上
this.element = Arrays.copyOf(this.element, this.capacity);
}
}
// 插入一个元素到线性表的第i个索引处
public void insert(int elem, int i) {
// 是否需要扩充容量
this.ensureCapicty(size + 1);
// 插入元素第i个位置空出来从i位置开始所有元素后移一个位置
System.arraycopy(this.element, i, this.element, i + 1, size - i);
// 将元素插入到指定位置
this.element[i] = elem;
// 当前容量增加1
this.size++;
}
// 在线性表末尾插入元素
public void add(int elem) {
this.insert(elem, this.size);
}
// 删除线性表中第i个元素并返回该处的值
public int delete(int i) {
if (i < 0 || i > size - 1) {// 检测删除位置对不对
throw new IndexOutOfBoundsException("删除位置索引越界:" + i);
}
// 获得i处的元素值
int del = (int) this.element[i];
// 删除元素后从i+1位置开始元素要前移
int moved = this.size - i - 1;// 需要移动元素的个数
if (moved > 0) {
System.arraycopy(this.element, i + 1, this.element, i, moved);
}
// 清空最后一个元素
this.element[--size] = null;
return del;
}
// 移除线性表中最后一个元素
public int remove() {
return this.delete(size - 1);
}
// 判断线性表是否为空
public boolean empty() {
return this.size == 0;
}
// 清空线性表
public void clear() {
// 将所有元素赋值为null
Arrays.fill(this.element, null);
this.size = 0;
}
// 覆写toString
public String toString() {
if (this.size == 0) {
return "[]";
} else {
// 返回字符串的字符串表示
StringBuffer sb = new StringBuffer("[");
for (int i = 0; i < this.size - 1; i++) {
sb.append(this.element[i].toString() + ",");
}
sb.append(this.element[this.size - 1].toString() + "]");
return sb.toString();
}
}
}
最后再附上一个测试线性表的例子
public static void main(String[] args) {
// TODO Auto-generated method stub
SquenceList list=new SquenceList();
list.add(0);
list.add(1);
list.add(2);
list.add(3);
list.insert(4, 0);
System.out.println(list);
System.out.println("index:3 ="+list.getelem(3));
System.out.println("data:3 ="+list.findindex(3));
list.delete(2);
System.out.println(list);
list.delete(0);
System.out.println(list);
list.delete(2);
System.out.println(list);
}
结语
好啦,线性表是数据结构里最简单的,所以比较轻松,但是也暗藏了一些需要注意的问题,下一篇:链表。