数据结构(java)之线性表
1. 线性表的抽象数据类型
a) 数据元素:任意同属一类对象的数据对象
b) 数据关系:线性关系,除第一个元素外,每个元素都有唯一的直接前驱元素,除最后一个元素外,每个元素都有唯一后继元素
c) 数据操作:定义在IList结构中,代码如下
publicinterface IList<E> {
boolean add(E e);
boolean insert(E e,intindex);
E remove(intindex);
int indexof(E e);
E get(intindex);
int size();
void clear();
boolean isEmpty();
boolean isFull();
}
2. 线性表实现之顺序表:使用MyArrayList类实现IList接口
a) 顺序表的存储结构:由一个数组存储,下标为0的元素为顺序表第一个元素
b) 顺序标基本操作
i. 初始化顺序表:创建一个空顺序表,申请一个存储大小为maxsize的数组空间,并将数组已存储元素个数size设置为0
ii. 向顺序表末尾添加元素:找到顺序表最后一个元素下标size,在第size+1个位置添加元素,并将size自增
iii. 删除元素:首先找到顺序表中数组下标为i的元素并将其存放在临时变量中,然后从第i个元素开始到最后一个元素,使每个元素值等于其下一个元素的指,然后将size自减,并返回临时变量
c) 代码实现
publicclass MyArrayList<E> implements IList<E> {
private E data[];
privateintmaxsize;
privateintsize;
// 构造方法初始化线性表
public MyArrayList(intmaxsize) {
super();
this.maxsize=maxsize;
this.data=(E[])new Object[maxsize]; //不能创建泛型数组,通过Object类来强转为泛型
this.size=0;
}
@Override
publicboolean add(E e) {
while(!this.isFull()) {
this.size++;
this.data[this.size]=e;
returntrue;
}
returnfalse;
}
@Override
//在下标为index的位置插入e
publicboolean insert(E e, intindex) {
this.check(index); //检查下表是否合法
while(!this.isFull()) {
for(inti=this.size;i>=index;i--) {
this.data[i+1]=data[i];
}
this.data[index]=e;
this.size++;
returntrue;
}
returnfalse;
}
@Override
//删除下标为index的元素
public E remove(intindex) {
this.check(index); //检查下标
E temp;
temp=this.data[index];
for(inti=index;i<this.size;i++) {
this.data[i]=this.data[i+1];
}
this.size--;
returntemp;
}
@Override
publicint indexof(E e) {
for(inti=1;i<=this.size;i++) {
if(this.data[i]==e)
returni;
}
return -1;
}
@Override
public E get(intindex) {
this.check(index); //检查下表
returnthis.data[index];
}
@Override
publicint size() {
returnthis.size;
}
@Override
publicvoid clear() {
this.size=0;
}
@Override
publicboolean isEmpty() {
if(this.size==0)
returntrue;
returnfalse;
}
@Override
publicboolean isFull() {
if(this.size>=this.maxsize-1)
returntrue;
returnfalse;
}
//判断输入的下标是否越界
publicvoid check(intindex) {
if(index<=0||index>this.size)
thrownew IndexOutOfBoundsException("index is "+index+",last element's index is "+(this.size-1));
}
}
3. 线性表实现之单链表:使用MyLinkList类实现IList接口
a) 单链表的存储结构:由节点组成,每个节点存储一个元素并指向下一个节点,每个单链表都有一个头节点,头节点中不存储数据元素,只存储指向下一个节点
b) 单链表的基本操作
i. 初始化单链表:为头节点申请空间,将头节点数据元素和下一节点元素都设为null,并将单链表元素个数size设为0
ii. 在末尾添加节点:首先通过遍历得到最后一个节点对象,然后申请一个新的节点,节点数据元素设位给定的数据,指向下一节点值设为null,然后将最后一个节点对象的next设为新申请的节点,并将单链表元素个数size自增
iii. 删除指定节点元素:通过遍历得到指定节点对象的上一个节点对象和下一个节点对象,然后将上一个节点的next设置为指定节点的下一个节点,将指定节点对象的next设为null,并返回指定节点的数据,然后将单链表节点个数size自减
c) 代码实现
class Node<E>{
private E e;
private Node<E> next;
public Node(E e, Node<E> next) {
super();
this.e = e;
this.next = next;
}
public E getE() {
returnthis.e;
}
publicvoid setE(E e) {
this.e = e;
}
public Node<E> getNext() {
returnthis.next;
}
publicvoid setNext(Node<E> next) {
this.next = next;
}
}
publicclass MyLinkList<E> implements IList<E> {
private Node<E> start;
privateintsize;
public MyLinkList() {
super();
this.start = new Node<E>(null,null);
this.size =0;
}
@Override
publicboolean add(E e) {
Node<E> temp=this.start;
while(temp.getNext()!=null){
temp=temp.getNext();
}
if(temp.getNext()==null) {
Node<E> node=new Node<E>(e,null);
temp.setNext(node);
this.size++;
returntrue;
}
else
returnfalse;
}
@Override
publicboolean insert(E e, intindex) {
if(index>this.size||index<0) {
thrownew IndexOutOfBoundsException("下标错误");
}
else {
Node<E> temp=this.start;
Node<E> node=new Node(e, null);
for(inti=0;i<index;i++) {
temp=temp.getNext();
}
Node<E> curr=temp.getNext();
temp.setNext(node);
node.setNext(curr);
this.size++;
returntrue;
}
}
@Override
public E remove(intindex) {
if(index>this.size||index<=0) {
thrownew IndexOutOfBoundsException("下标错误");
}
else {
Node<E> temp=this.start;
for(inti=1;i<index;i++) {
temp=temp.getNext();
}
Node<E> node=temp.getNext();
Node<E> curr=temp.getNext().getNext();
temp.setNext(curr);
node.setNext(null);
this.size--;
returnnode.getE();
}
}
@Override
publicint indexof(E e) {
Node<E> temp=this.start;
intindex=0;
while(temp.getNext()!=null) {
temp=temp.getNext();
index++;
if(temp.getE()==e) {
returnindex;
}
}
return -1;
}
@Override
public E get(intindex) {
if(index>this.size||index<=0) {
thrownew IndexOutOfBoundsException("下标错误");
}
Node<E> temp=this.start;
for(inti=1;i<=index;i++) {
temp=temp.getNext();
}
returntemp.getE();
}
@Override
publicint size() {
returnthis.size;
}
@Override
publicvoid clear() {
this.start.setNext(null);
this.size=0;
}
@Override
publicboolean isEmpty() {
if(this.size==0)
returntrue;
returnfalse;
}
@Override
publicboolean isFull() {
// TODO Auto-generated method stub
returnfalse;
}
}
4. 线性表实现之双向链表
a) 双向链表的存储结构:由节点组成,每个节点由数据元素以及指向前一个节点的pre和指向后一个节点的next组成,每个双向链表都有一个头节点,头节点的数据元素和pre都是null,next指向下一个元素
b) 双向链表的基本操作
i. 初始化双向链表:为头节点申请空间,并将头节点的数据元素、pre、next都设为null,链表节点个数size设为0
ii. 向末尾添加元素:为新节点申请空间,并将数据设为指定数据,将链表最后一个元素的next指向新节点,将新节点的pre设为链表最后一个元素,新节点的next设为null
5. 线性表实现之循环链表:将最后一个元素的next指向头节点的单向链表
6. 应用:约瑟夫环
a) 约瑟夫环:假设编号从1、2、3…n的n个人顺时针围坐一圈,每人持有随机生成的一个密码m(1-5),从指定编号为1的位置开始报数,到1号的密码m位置,m出列,并将他的密码作为m,他的下一个位置为1重新开始报数,知道所有人全部出列,设计一个程序求出出列顺序
b) 代码实现
//约瑟夫节点,包括号码和密码
class JosephusNode {
privateintno;
privateintpwd;
public JosephusNode(intno, intpwd) {
super();
this.no = no;
this.pwd = pwd;
}
publicint getNo() {
return no;
}
publicvoid setNo(intno) {
this.no = no;
}
publicint getPwd() {
returnpwd;
}
publicvoid setPwd(intpwd) {
this.pwd = pwd;
}
@Override
public String toString() {
return"("+this.no+","+this.pwd+")";
}
}
publicclass Josephus {
private IList<JosephusNode> list;
privateintnum;
//初始化约瑟夫环,为环中每个节点分配no和1-5的密码
public Josephus(IList<JosephusNode> list,intnum) {
super();
this.list = list;
this.num=num;
for(inti=1;i<=num;i++) {
intno=i;
intpwd=(int)(Math.random()*5)+1;
list.add(new JosephusNode(no,pwd));
}
}
publicint getNum() {
returnnum;
}
publicvoid setNum(intnum) {
this.num = num;
}
//返回环中序号为index的节点
public JosephusNode get(intindex) {
returnlist.get(index);
}
//出环游戏
publicvoid game() {
intstart=1; //从1号开始
intcnt;
System.out.println("游戏开始");
for(inti=1;i<=this.num;i++) {
//出列号码是start开始计数start对应的密码位
cnt=(start+this.list.get(start).getPwd()-2)%this.list.size()+1;
System.out.println(this.list.remove(cnt));
if(this.list.size()==0)
break;
start=(cnt-1)%this.list.size()+1;
}
}
}
7. 不同结构的线性表复杂度分析
a) 顺序表
i. 插入:boolean insert(E e,int index);顺序表的插入时间主要耗费在移动数据元素上,时间复杂度为O(n)
ii. 删除:E remove(int index);顺序表的删除时间主要耗费在移动数据元素上,时间复杂度为O(n)
iii. 取表元素:E get(int index);顺序表的取表元素只需要按照给定序号查找对应数组元素,时间复杂度为O(1)
iv. 定位元素:int indexOf(E e);顺序表的定位时间主要耗费在元素比较上,时间复杂度为O(n)
b) 单链表
i. 插入:boolean insert(E e,int index);单链表的插入时间主要耗费在查找插入位置上,时间复杂度为O(n)
ii. 删除:E remove(int index);单链表的删除时间主要耗费在查找删除位置上,时间复杂度为O(n)
iii. 取表元素:E get(int index);单链表的取表元素时间主要耗费在查找取表元素位置上,时间复杂度为O(n)
iv. 定位元素:int indexOf(E e);单链表的定位时间主要耗费在元素比较上,时间复杂度为O(n)
c) 总结:由于顺序表的元素存储是连续的,所以查找很方便,效率很高,但是插入和删除需要移动大量的元素,效率很低;单链表中由于数据存储是不连续的,所以插入和删除的效率很高,但是查找需要从头开始遍历,所以效率较低。因此,如果需要进行大量的查找等操作而不经常插入或删除,采用顺序表,反之,采用链表。
1. 线性表的抽象数据类型
a) 数据元素:任意同属一类对象的数据对象
b) 数据关系:线性关系,除第一个元素外,每个元素都有唯一的直接前驱元素,除最后一个元素外,每个元素都有唯一后继元素
c) 数据操作:定义在IList结构中,代码如下
publicinterface IList<E> {
boolean add(E e);
boolean insert(E e,intindex);
E remove(intindex);
int indexof(E e);
E get(intindex);
int size();
void clear();
boolean isEmpty();
boolean isFull();
}
2. 线性表实现之顺序表:使用MyArrayList类实现IList接口
a) 顺序表的存储结构:由一个数组存储,下标为0的元素为顺序表第一个元素
b) 顺序标基本操作
i. 初始化顺序表:创建一个空顺序表,申请一个存储大小为maxsize的数组空间,并将数组已存储元素个数size设置为0
ii. 向顺序表末尾添加元素:找到顺序表最后一个元素下标size,在第size+1个位置添加元素,并将size自增
iii. 删除元素:首先找到顺序表中数组下标为i的元素并将其存放在临时变量中,然后从第i个元素开始到最后一个元素,使每个元素值等于其下一个元素的指,然后将size自减,并返回临时变量
c) 代码实现
publicclass MyArrayList<E> implements IList<E> {
private E data[];
privateintmaxsize;
privateintsize;
// 构造方法初始化线性表
public MyArrayList(intmaxsize) {
super();
this.maxsize=maxsize;
this.data=(E[])new Object[maxsize]; //不能创建泛型数组,通过Object类来强转为泛型
this.size=0;
}
@Override
publicboolean add(E e) {
while(!this.isFull()) {
this.size++;
this.data[this.size]=e;
returntrue;
}
returnfalse;
}
@Override
//在下标为index的位置插入e
publicboolean insert(E e, intindex) {
this.check(index); //检查下表是否合法
while(!this.isFull()) {
for(inti=this.size;i>=index;i--) {
this.data[i+1]=data[i];
}
this.data[index]=e;
this.size++;
returntrue;
}
returnfalse;
}
@Override
//删除下标为index的元素
public E remove(intindex) {
this.check(index); //检查下标
E temp;
temp=this.data[index];
for(inti=index;i<this.size;i++) {
this.data[i]=this.data[i+1];
}
this.size--;
returntemp;
}
@Override
publicint indexof(E e) {
for(inti=1;i<=this.size;i++) {
if(this.data[i]==e)
returni;
}
return -1;
}
@Override
public E get(intindex) {
this.check(index); //检查下表
returnthis.data[index];
}
@Override
publicint size() {
returnthis.size;
}
@Override
publicvoid clear() {
this.size=0;
}
@Override
publicboolean isEmpty() {
if(this.size==0)
returntrue;
returnfalse;
}
@Override
publicboolean isFull() {
if(this.size>=this.maxsize-1)
returntrue;
returnfalse;
}
//判断输入的下标是否越界
publicvoid check(intindex) {
if(index<=0||index>this.size)
thrownew IndexOutOfBoundsException("index is "+index+",last element's index is "+(this.size-1));
}
}
3. 线性表实现之单链表:使用MyLinkList类实现IList接口
a) 单链表的存储结构:由节点组成,每个节点存储一个元素并指向下一个节点,每个单链表都有一个头节点,头节点中不存储数据元素,只存储指向下一个节点
b) 单链表的基本操作
i. 初始化单链表:为头节点申请空间,将头节点数据元素和下一节点元素都设为null,并将单链表元素个数size设为0
ii. 在末尾添加节点:首先通过遍历得到最后一个节点对象,然后申请一个新的节点,节点数据元素设位给定的数据,指向下一节点值设为null,然后将最后一个节点对象的next设为新申请的节点,并将单链表元素个数size自增
iii. 删除指定节点元素:通过遍历得到指定节点对象的上一个节点对象和下一个节点对象,然后将上一个节点的next设置为指定节点的下一个节点,将指定节点对象的next设为null,并返回指定节点的数据,然后将单链表节点个数size自减
c) 代码实现
class Node<E>{
private E e;
private Node<E> next;
public Node(E e, Node<E> next) {
super();
this.e = e;
this.next = next;
}
public E getE() {
returnthis.e;
}
publicvoid setE(E e) {
this.e = e;
}
public Node<E> getNext() {
returnthis.next;
}
publicvoid setNext(Node<E> next) {
this.next = next;
}
}
publicclass MyLinkList<E> implements IList<E> {
private Node<E> start;
privateintsize;
public MyLinkList() {
super();
this.start = new Node<E>(null,null);
this.size =0;
}
@Override
publicboolean add(E e) {
Node<E> temp=this.start;
while(temp.getNext()!=null){
temp=temp.getNext();
}
if(temp.getNext()==null) {
Node<E> node=new Node<E>(e,null);
temp.setNext(node);
this.size++;
returntrue;
}
else
returnfalse;
}
@Override
publicboolean insert(E e, intindex) {
if(index>this.size||index<0) {
thrownew IndexOutOfBoundsException("下标错误");
}
else {
Node<E> temp=this.start;
Node<E> node=new Node(e, null);
for(inti=0;i<index;i++) {
temp=temp.getNext();
}
Node<E> curr=temp.getNext();
temp.setNext(node);
node.setNext(curr);
this.size++;
returntrue;
}
}
@Override
public E remove(intindex) {
if(index>this.size||index<=0) {
thrownew IndexOutOfBoundsException("下标错误");
}
else {
Node<E> temp=this.start;
for(inti=1;i<index;i++) {
temp=temp.getNext();
}
Node<E> node=temp.getNext();
Node<E> curr=temp.getNext().getNext();
temp.setNext(curr);
node.setNext(null);
this.size--;
returnnode.getE();
}
}
@Override
publicint indexof(E e) {
Node<E> temp=this.start;
intindex=0;
while(temp.getNext()!=null) {
temp=temp.getNext();
index++;
if(temp.getE()==e) {
returnindex;
}
}
return -1;
}
@Override
public E get(intindex) {
if(index>this.size||index<=0) {
thrownew IndexOutOfBoundsException("下标错误");
}
Node<E> temp=this.start;
for(inti=1;i<=index;i++) {
temp=temp.getNext();
}
returntemp.getE();
}
@Override
publicint size() {
returnthis.size;
}
@Override
publicvoid clear() {
this.start.setNext(null);
this.size=0;
}
@Override
publicboolean isEmpty() {
if(this.size==0)
returntrue;
returnfalse;
}
@Override
publicboolean isFull() {
// TODO Auto-generated method stub
returnfalse;
}
}
4. 线性表实现之双向链表
a) 双向链表的存储结构:由节点组成,每个节点由数据元素以及指向前一个节点的pre和指向后一个节点的next组成,每个双向链表都有一个头节点,头节点的数据元素和pre都是null,next指向下一个元素
b) 双向链表的基本操作
i. 初始化双向链表:为头节点申请空间,并将头节点的数据元素、pre、next都设为null,链表节点个数size设为0
ii. 向末尾添加元素:为新节点申请空间,并将数据设为指定数据,将链表最后一个元素的next指向新节点,将新节点的pre设为链表最后一个元素,新节点的next设为null
5. 线性表实现之循环链表:将最后一个元素的next指向头节点的单向链表
6. 应用:约瑟夫环
a) 约瑟夫环:假设编号从1、2、3…n的n个人顺时针围坐一圈,每人持有随机生成的一个密码m(1-5),从指定编号为1的位置开始报数,到1号的密码m位置,m出列,并将他的密码作为m,他的下一个位置为1重新开始报数,知道所有人全部出列,设计一个程序求出出列顺序
b) 代码实现
//约瑟夫节点,包括号码和密码
class JosephusNode {
privateintno;
privateintpwd;
public JosephusNode(intno, intpwd) {
super();
this.no = no;
this.pwd = pwd;
}
publicint getNo() {
return no;
}
publicvoid setNo(intno) {
this.no = no;
}
publicint getPwd() {
returnpwd;
}
publicvoid setPwd(intpwd) {
this.pwd = pwd;
}
@Override
public String toString() {
return"("+this.no+","+this.pwd+")";
}
}
publicclass Josephus {
private IList<JosephusNode> list;
privateintnum;
//初始化约瑟夫环,为环中每个节点分配no和1-5的密码
public Josephus(IList<JosephusNode> list,intnum) {
super();
this.list = list;
this.num=num;
for(inti=1;i<=num;i++) {
intno=i;
intpwd=(int)(Math.random()*5)+1;
list.add(new JosephusNode(no,pwd));
}
}
publicint getNum() {
returnnum;
}
publicvoid setNum(intnum) {
this.num = num;
}
//返回环中序号为index的节点
public JosephusNode get(intindex) {
returnlist.get(index);
}
//出环游戏
publicvoid game() {
intstart=1; //从1号开始
intcnt;
System.out.println("游戏开始");
for(inti=1;i<=this.num;i++) {
//出列号码是start开始计数start对应的密码位
cnt=(start+this.list.get(start).getPwd()-2)%this.list.size()+1;
System.out.println(this.list.remove(cnt));
if(this.list.size()==0)
break;
start=(cnt-1)%this.list.size()+1;
}
}
}
7. 不同结构的线性表复杂度分析
a) 顺序表
i. 插入:boolean insert(E e,int index);顺序表的插入时间主要耗费在移动数据元素上,时间复杂度为O(n)
ii. 删除:E remove(int index);顺序表的删除时间主要耗费在移动数据元素上,时间复杂度为O(n)
iii. 取表元素:E get(int index);顺序表的取表元素只需要按照给定序号查找对应数组元素,时间复杂度为O(1)
iv. 定位元素:int indexOf(E e);顺序表的定位时间主要耗费在元素比较上,时间复杂度为O(n)
b) 单链表
i. 插入:boolean insert(E e,int index);单链表的插入时间主要耗费在查找插入位置上,时间复杂度为O(n)
ii. 删除:E remove(int index);单链表的删除时间主要耗费在查找删除位置上,时间复杂度为O(n)
iii. 取表元素:E get(int index);单链表的取表元素时间主要耗费在查找取表元素位置上,时间复杂度为O(n)
iv. 定位元素:int indexOf(E e);单链表的定位时间主要耗费在元素比较上,时间复杂度为O(n)
c) 总结:由于顺序表的元素存储是连续的,所以查找很方便,效率很高,但是插入和删除需要移动大量的元素,效率很低;单链表中由于数据存储是不连续的,所以插入和删除的效率很高,但是查找需要从头开始遍历,所以效率较低。因此,如果需要进行大量的查找等操作而不经常插入或删除,采用顺序表,反之,采用链表。