一、线性表
1、基本概念
线性表是一组同类型数据的集合,逻辑结构为线性结构,对于任何一个非空线性表都有以下特点
- 有且只有一个结点无前驱(头结点)
- 有且只有一个结点无后进(尾结点)
- 除头结点外,其他结点有且只有一个前驱
- 除尾结点外,其他结点有且只有一个后进
注意:线性表的定义只是逻辑上是线性结构,物理上不一定有关系
二、顺序表
1、概念
顺序表的本质是一个顺序存储的线性表,是用一组地址连续的存储单元依次存储线性表的数据元素,一般情况使用数组储存,特点是逻辑上相邻的元素,其物理次序也是相邻的。顺序表一般可分为:
- 静态顺序表:使用定长数组存储。
- 动态顺序表:使用动态开辟的数组存储。
2、实现功能
顺序表的的功能实现比较简单,就相当于对数组的增删查改,Java集合框架中自带的类ArrayList就是一个顺序表。
(1)、插入
插入分为两种,一种是末尾插入,一种是中间插入。
因为是一个数组,所以中间元素的插入会带动其他元素的移动。
public void add(int pos, int data) {
if (isFull()){
System.out.println("顺序表已满,需要扩充");
this.elem = Arrays.copyOf(elem,elem.length*2);
}
if (pos < 0||pos > this.usedSize){
return;
}
for (int i = usedSize-1 ; i <= pos ; i--) {
elem[i+1] = elem[i];
}
elem[pos] = data;
usedSize++;
}
(2)、删除
删除和插入大致上相同,也分为两种,一种是删除末尾元素,一种是删除中间元素
public void remove(int toRemove) {
for (int i = 0; i < this.usedSize - 1; i++) {
if (elem[i] == toRemove) {
for (int j = i; j < this.usedSize; j++) {
elem[j] = elem[j + 1];
}
usedSize--;
break;
}
}
if (elem[usedSize - 1] == toRemove) {
usedSize--;
return;
}
}
(3)、查找
查找有两种方式,一种是给下标找值,另一种是给值找下标,因为本质是一个数组,下标可以直接找到,所以第一种方式很简单,第二种也只需要遍历数组,找到元素就可以了。
//给下标找值
public int getPos(int pos) {
if (pos < 0||pos > this.usedSize-1){
return -1;
}
return elem[pos];
}
//给值找下标
public int search(int toFind) {
for (int i = 0; i <this.usedSize-1 ; i++) {
if (elem[i] == toFind){
return i;
}
}
return -1;
}
(4)、修改
修改其实就是在查找的基础上稍加改动,把寻找到的值做出修改就完成了。
public void setPos(int pos, int value) {
if (pos < 0||pos > this.usedSize-1){
return ;
}
elem[pos] = value;
}
三、链表
1、概念
链表本质也是一个线性表,是一个链式存储的线性表,链表可以用一组任意的储存单元储存线性表的数据元素(这组存储单元可以是连续的也可以是不连续的),因此,为了表示每个元素和它后一个元素之间的逻辑关系,我们在存储数据的同时,还会存储下一个存储单元的地址。这两部分信息组成元素的存储映像,称为结点。它包含两个域,数据域和指针域,根据指针域中指针的个数、指向,我们可以把链表分为单向链表、双向链表、循环链表等等,还有带头结点和不带头头结点的链表,Java集合框架中LikeList底层是一个双向链表,我们这里主要介绍的是单向不带头链表。
注意:200、400均为结点的地址,本为16进制,为画图方便,采用了简化。
单向链表的特点
- 需要一个头指针指向头结点
- 尾结点中指针域为null
- 除头结点以外,其余结点的地址存放在前驱的指针域
双向链表的特点
- 头结点中prev域为null
- 需要一个尾指针指向尾结点
循环链表的特点
- 尾结点的指针域不再是null,而是存放头结点的地址
2、功能实现
(1)、插入
与顺序表相比,链表的插入不需要频繁的移动元素,但是时间复杂度上却都是O(n),因为链表中地址不连续我们不能直接找到某一下标的地址,需要从头结点开始遍历链表,相当于查找的时间复杂度为O(n),实际插入的时间复杂度是O(1),查找和搬移元素相比较,肯定还是查找更快,所以尽管时间复杂度相同,但是对插入这一操作,链表的表现会优于顺序表。
插入分类:
- 头插
- 中间插入(包含尾插)
//头插
public void addfirst(int a){
Node node = new Node(a);
node.next = this.head;
this.head = node;
}
//尾插
public void addlest(int a) {
Node node = new Node(a);
Node cur = this.head;
if (this.head == null) {
this.head = node;
} else {
while (cur.next != null) {
cur = cur.next;
}
cur.next = node;
}
}
//中间插
public void addIndex(int Index ,int a){
Node node = new Node(a);
if (Index == 0){
node.next = this.head;
this.head = node;
return;
}
if (Index < 0 ||Index > getlence()){
System.out.println("Index 位置不合法");
return;
}
Node cur = this.head;
while (Index-1 != 0){
cur = cur.next;
Index--;
}
node.next = cur.next;
cur.next = node;
}
(2)、删除
删除操作相比较插入操作就会复杂许多,其中主要的问题就是当你找到要删除结点时,因为是链表,所以你无法知道你的前一个结点的地址是什么,所以我们有两种方法来删除元素
方法一:记录结点法,首先判断头结点是否是要删除结点,如果不是,定义一个cur指针指向头结点的下一个结点,再定义一个前驱指针prev指向头结点,每次判断cur结点的值是否是要删除的元素,若不是则让cur和prev同时指向当前结点的下一个结点,直到找到要删除结点。
public void reMove(int key){
if (this.head == null){
return;
}
if(this.head.data == key){
this.head = this.head.next;
return;
}
Node cur = this.head.next;
Node prev = head;
while (cur != null){
if (cur.data == key){
prev.next = cur.next;
cur = cur.next;
}else {
prev = cur;
cur = cur.next;
}
}
}
方法二:替罪法,首先查找待删元素的结点地址,然后让待删结点的下一结点的元素与该结点互换,最后删除下一结点。
public void reMove(int key){
if(this.head == null){
return;
}
Node cur = this.head;
while(cur.data == key){
Node del = cur.next;
cur.data = del.data;
cur.next = del.next;
}
}
(3)、查找
查找相对前面的操作就比较简单了,链表一般很少用到下标,所以都是给元素找地址,或者还有一种就是判断链表中是否包含这个元素,也属于查找的一种。
public boolean contains(int key){
Node cur = this.head;
while(cur != null){
if(cur.data == key){
return true;
}
}
return false;
}
四、总结
顺序表和链表各有千秋,不能笼统的说谁好谁坏,要参考对应的情况,比如插入删除操作频繁的方法就需要同链表,比如查找元素频繁或者对查找有时间限制的情况就需要用到顺序表。详细的对比我会在后面再做一个总结!!!