线性表
线性表的特性:数据元素之间具有一种“一对一”的逻辑关系。
- 第一个数据元素没有前驱,这个数据元素被称为头结点;
- 最后一个数据元素没有后继,这个数据元素被称为尾结点;
- 除了第一个和最后一个数据元素外,其他数据元素有且仅有一个前驱和一个后继。
如果把线性表用数学语言来定义,则可以表示为(a1,…ai-1,ai,ai+1,…an),ai-1领先于ai,ai领先于
ai+1,称ai-1是a的前驱元素,ai+1是ai的后继元素
线性表的分类
线性表中数据存储的方式可以是顺序存储,也可以链式存储,按照数据的存储方式不同,可以把线性表分为顺序表和链表。
顺序表
顺序表是在计算机内存中以数组的形式保存的线性表,线性表的顺序存储是指用一组连续的存储单位,依次存储线性表中的各个元素,使得线性表在逻辑上相邻的数据元素存储在相邻的物理单元中,即通过数据元素物理存储的相邻关系来反映数据元素之间逻辑上的相邻关系。
线性表的顺序表的实现:
顺序表的遍历:
一般作为容器存储数据,都需要向外部提供遍历的方式,因此我们需要给顺序表提供遍历方式
在java中,遍历集合的方式一般都是用的是foreach循环,如果想让我们的SequenceList也能支持foreach循环,则需要做如下操作:
- 让SequenceList实现Iterator接口,重写iterator方法
- 在SequenceList内部提供一个内部类Iterator,实现iterator接口,重写hasNext方法和next方法;
- 代码实现
顺序表的容量可变:
当我们使用SequenceList时,先new SequenceList(5)创建一个对象,创建对象时就需要指定容器的大小,初始化指定大小的数组来存储元素,当我们插入元素时,如果已经插入了5个元素,还要继续插入数据,则会报错,就不能插入了。这种设计不符合容器的设计理念,因此我们在设计顺序时,应该考虑它的容量的伸缩性。
考虑容器的容量伸缩性,其实就是改变存储数据元素的数组的大小,那我们需要考虑什么时候需要改变数组的大小?
-
添加元素时:
添加元素时,应该检查当前数组的大小是否能容纳新的元素,如果不能容纳,则需要创建新的容量更大的数组,我们这里创建一个是原数组两倍容量的新数组存储元素。
2.移除元素时:
移除元素,应该检查当前数组的大小是否太大,比如正在用100个容量的数组存储10个元素,这样就会造成内存空间的浪费,应该创建一个容量更小的数组存储元素。如果我们发现数据元素的数量不足数组容量的1/4,则创建一个是原数组容量的1/2的新数组存储元素。
顺序表的时间复杂度
get(i):不难看出,不论数据元素量N有多大,只需要一次eles[i]就可以获取到对应的元素,所以时间复杂度为O(1);
insert(int i,T t):每一次插入,都需要把i后面的元素移动一次,随着元素数量N的增大,移动的元素也越多,时间复杂度为O(n);
remove(int i):每一次删除,都需要把i位置后面移动一次,随着数据量N的增大,移动的元素也会越多,时间复杂度为O(n);
由于顺序表的底层由数组实现,数组的长度是固定的,所以在操作的过程中涉及到了容器扩容操作。这样会导致顺序表在使用过程中的时间复杂度不是线性的,在某个需要扩容的结点处,耗时会突增,尤其是元素越多,这个问题越明显。
代码实现
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(eles.length*2);
}
eles[N++]=t;
}
// 在i元素处插入元素t
public void insert(int i,T t){
if (N==eles.length){
resize(eles.length*2);
}
// 将i位置之后的所有元素向后移动
for(int index=N;index>i;index--){
eles[index]=eles[index-1];
}
N++;
eles[i]=t;
}
// 根据参数newSize,重置eles的大小
public void resize(int newSize){
// 定义一个临时数组,指向原数组
T[] temp=eles;
// 创建新数组
eles=(T[])new Object[newSize];
// 将原数组的数据拷贝到新数组即可
for (int index=0;index<temp.length;index++){
eles[index]=temp[index];
}
}
// 删除指定位置i处的元素,并返回该元素
public T remove(int i){
T t=eles[i];
for (int j=i;j<eles.length-1;j++){
eles[j]=eles[j+1];
}
N--;
if (N<eles.length/4){
resize(eles.length/2);
}
return t;
}
// 查找t元素第一次出现的位置
public int indexOf(T t){
for (int i=0;i<eles.length;i++){
if (eles[i].equals(t)){
return i;
}
}
return -1;
}
@Override
public Iterator<T> iterator() {
return new ListIterator<T>();
}
private class ListIterator<T> implements Iterator<T>{
private int index;
public ListIterator() {
this.index = 0;
}
@Override
public boolean hasNext() {
return index<N;
}
@Override
public T next() {
return (T) eles[index++];
}
}
链表
虽然顺序表的查询很快,时间复杂度为O(1),但是增删的效率是比较低的,因为每一次增删操作都伴随着大量的数据元素移动。这个问题有没有解决方案呢?有,我们可以使用另外一种存储结构实现线性表,链式存储结构。
链表是一种物理存储单元上非连续,非顺序的存储结构,其物理结构不能表示数据元素的逻辑顺序,数据元素的逻辑顺序是通过链表中的指针链接次序实现。链表由一系列的结点(链表中的每一个元素称为结点)组成,结点可以在运行时动态生成。
单向链表
单向链表是链表的一种,它由多个结点组成,每一个结点都由一个数据域和一个指针域组成,数据域用来存储数据,指针域用来指向其后继结点。链表的头结点的数据域不存储数据,指针域指向第一个真正存储数据的结点。
代码实现:
package com.cdcas.day5;
import java.util.Iterator;
/**
* 单向链表的实现
*/
public class LinkList<T> implements Iterable<T>{
// 记录头结点
private Node<T> head;
// 记录链表的长度
private int N;
// 结点类
private class Node<T>{
// 存储数据
public T item;
// 指向下一个结点
public Node<T> next;
public Node(T item, Node<T> next) {
this.item = item;
this.next = next;
}
}
public LinkList(){
//初始化头结点
this.head=new Node<>(null,null);
//初始化元素个数
this.N=0;
}
// 清空链表
public void clear(){
this.head.next=null;
this.N=0;
}
// 获取链表的长度
public int length(){
return N;
}
// 判断链表是否为空
public boolean isEmpty(){
return N==0;
}
// 获取指定位置i处的元素
public T get(int i){
Node<T> node=head;
for (int j=0;j<=i;j++){
node=node.next;
}
return node.item;
}
// 向链表中添加元素t
public void insert(T t){
Node<T> node=head;
while (node.next!=null){
node=node.next;
}
node.next= new Node<>(t,null);
N++;
}
// 向指定位置i出,添加元素t
public void insert(int i,T t){
//找到i-1位置的结点
Node<T> node=head;
for (int j=0;j<i;j++){
node=node.next;
}
Node<T> newNode=new Node<>(t,null);
newNode.next=node.next;
node.next=newNode;
N++;
}
// 删除指定位置i处的元素,并返回被删除的元素
public T remove(int i){
Node<T> node=head;
for (int j=0;j<i;j++){
head=head.next;
}
Node<T> nodei=node.next;
node.next=nodei.next;
N--;
return nodei.item;
}
// 查找元素l在链表中第一次出现的位置
public int indexOf(T t){
int count = 0;
Node<T> node=head;
while (node.item!=t){
node=node.next;
count++;
}
return count;
}
@Override
public Iterator<T> iterator() {
return new LIterator();
}
private class LIterator implements Iterator<T>{
private Node<T> node;
public LIterator(){
this.node=head;
}
@Override
public boolean hasNext() {
return node.next!=null;
}
@Override
public T next() {
node=node.next;
return node.item;
}
}
}
双向链表:
双向链表也叫双向表,是链表的一种,它由多个结点组成,每个结点都由一个数据域和两个指针域组成,数据域用来存储数据,其中一个指针域用来指向其后继结点,另一个指针域用来指向前驱结点。链表的头结点的数据域不存储数据,指向前驱结点的指针域为null,指向后继节点的指针域指向第一个真正存储数据的结点。
代码实现:
public class TowWayLinkList<T> implements Iterable<T>{
//首结点
private Node<T> head;
//最后一个结点
private Node<T> last;
//链表的长度
private int N;
//定义结点
private static class Node<T>{
//存储数据
public T item;
//指向上一个结点
public Node<T> pre;
//指向下一个结点
public Node<T> next;
public Node(T item,Node<T> pre,Node<T> next) {
this.item=item;
this.pre=pre;
this.next=next;
}
}
public TowWayLinkList() {
//初始化头结点和尾结点
this.head=new Node<>(null,null,null);
this.last=null;
//初始化元素个数
N=0;
}
//清空链表
public void clear(){
this.head.next=null;
this.last.next=null;
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<T> node=new Node<>(t,head,null);
//让新结点称为尾结点
last=node;
//让头结点指向尾结点
head.next=last;
}else {
//如果不为空
Node<T> node=new Node<>(t,last,null);
//让尾结点指向新结点
last.next=node;
//让新插入的结点成为尾结点
last=node;
}
//让元素个数加一
N++;
}
//向指定位置i处插入元素t
public void insert(int i,T t){
Node<T> node=head;
for (int j=0;j<i;j++){
node=node.next;
}
//i位置上的结点
Node<T> nodeI=node.next;
Node<T> newNode=new Node<>(t,node,nodeI);
node.next=newNode;
nodeI.pre=newNode;
N++;
}
//获取指定位置i处的元素
public T get(int i){
Node<T> node=head;
for (int j=0;j<=i;j++){
node=node.next;
}
return node.item;
}
//找到元素t在链表中第一次出现的位置
public int indexOf(T t){
Node<T> node=head;
int count=0;
while (node.next!=null){
node=node.next;
count++;
}
return count;
}
//删除位置i处的元素,并返回该元素
public T remove(int i){
Node<T> node=head;
for (int j=0;j<i;j++){
node=node.next;
}
//找到i位置的结点
Node<T> nodeI=node.next;
//找到i位置的下一个结点
Node<T> nextNodeI=nodeI.next;
node.next=nextNodeI;
nextNodeI.pre=node;
N--;
return nodeI.item;
}
@Override
public Iterator<T> iterator() {
return new TowLinkIterator();
}
private class TowLinkIterator implements Iterator<T>{
private Node<T> node;
public TowLinkIterator() {
this.node = head;
}
@Override
public boolean hasNext() {
return node.next!=null;
}
@Override
public T next() {
node=node.next;
return node.item;
}
}
}