第三章.线性表
3.1线性表的定义
线性表是一种基本的数据结构,它是由零个或多个数据元素组成的有限序列。线性表中的元素具有相同的数据类型,并且元素之间存在着一对一的线性关系
线性表元素的个数n(n>0)定义为线性表的长度,当n=0时,称为空表
线性表的逻辑特征
- 在非空的线性表,有且仅有一个开始节点a1,它没有直接前驱,而仅有一个直接后继a2;
- 有且仅有一个终端节点an,它没有直接后继,而仅有一个直接前驱an-1;
- 其余的内部节点ai(2≤i≤n-1)都有且仅有一个直接前驱ai-1和一个直接后继ai+1
3.2线性表的抽象数据类型
ADT List{
数据对象:
D={ai|ai属于Elemset,(i=1,2,3,···,n,n≥0)}
数据关系:
R={<ai-1,ai>|ai-1,ai属于D,(i=2,3,···,n)}
基本操作:
InitList(&L); //构造一个空的线性表L
DestoryList(&L); //当线性表L已经存在,销毁线性表L
ClearList(&L); //当线性表已经存在,将线性表L重置为空表
ListEmpty(L); //当线性表已经存在,若线性表为空表,则返回为TRUE,否则返回FALSE
ListLength(L); //当线性表已经存在,返回线性表L中的数据元素个数
·······
3.3线性表的顺序存储结构
3.3.1顺序存储定义
顺序存储定义:把逻辑上相邻的数据元素存储在物理上相邻的存储单元中的存储结构
线性表的顺序存储结构,指的是用一段地址连续的存储单元依次存储线性表的数据元素
3.3.2顺序存储方式
线性表的顺序存储方式可以选择数组
- 连续存储空间:数组在内存中的存储空间是连续的,每个元素的物理位置与逻辑位置一一对应,这使得我们可以直接通过下标进行访问
- 随机访问能力:由于数组的存储空间是连续的,因此可以通过下标随机访问数组中的任意元素,这使得线性表的访问操作非常高效
- 数组长度可变:虽然数组的长度是固定的,但是我们可以通过创建一个新的更大的数组,将原数组中的元素复制到新数组中,从而实现数组长度的动态增长(Java可以通过Arrays类中的copeOf方法)
- 内存高效利用:由于数组的存储空间是连续的,因此在内存中分配数组所需的空间非常高效,不需要像链式存储结构那样需要额外的空间来存储空间
3.3.3数组长度与线性表长度区别
- 数组的长度是存放线性表的存储空间的长度,存储分配后这个量一般是不变的,当存储空间不足时,可以动态分配
- 线性表的长度是线性表中数据元素的个数,随着线性表插入和删除操作的进行,这个量是变化的
- 在任意时刻,线性表的长度应该小于等于数组的长度
3.3.4地址计算
1.假设线性表的每个元素需占m个存储单元,则第i+1个数据元素的存储位置和第i个数据元素的存储位置之间满足关系: LOC(ai+1)=LOC(ai)+m
2.所有数据元素的存储位置均可由第一个数据元素的存储位置得到: LOC(ai)=LOC(a1)+(i-1)*m
3.3.5顺序表基本操作的实现
定义一个顺序表:
import java.util.Arrays;
public class List {
private int[] data;//存储元素的数组
private int size;//当前线性表当中元素个数
public int[] getData() {
return data;
}
public void setData(int[] data) {
this.data = data;
}
public int getSize() {
return size;
}
public void setSize(int size) {
this.size = size;
}
//初始化操作
public List(int capacity){
data=new int[capacity];
size=0;
}
public List(){
this(10);//创建一个默认容量为10的数组
}
//销毁已存在的线性表
public void destoryList(){
if (data!=null){
data=null;
}
}
//清空线性表
public void clearList(){
int[] data = this.data;
for (int i=0;i<size;i++) {
data[i]=0;
}
size=0;
}
//在线性表中第i个位置插入新元素e
//思路:
// 1.首先判断给定的位置是否合法,由于是顺序存储,插入的位置不能小于0或者大于线性表元素个数(由于采用的是数组存储,元素从0开始存储)
// 2.判断当前数组是否还有空余空间,如果没有使用Arrays类的copyOf方法增加数组存储空间
// 3.插入数据时,数据所在位置之后的元素都需要往后移一位,包括所在位置的元素
// 4.将要插入的元素放入到指定位置
// 5.线性表元素个数加一
public void listInsert(int i,int e){
if (i<0||i>size) {
throw new IllegalArgumentException("超出范围");
}
if(i==data.length){
//当数组空间不足时,对数组进行扩容
data= Arrays.copyOf(data, (int) (1.5 * data.length));
}
for (int a=size-1;a>=i;a--){
data[a+1]=data[a];
}
data[i]=e;
size++;
}
//删除线性表中的第i个位置元素,返回e
//思路:
// 1.首先判断给定的位置是否合法,由于是删除元素,取值范围在1到size
// 2.将欲取出来的数据放入到e当中
// 3.将取出元素位置之后的元素都往前移一位
// 4.检查size-1的位置是否为0
// 5.线性表元素个数减一
public int listDelet(int i){
int e=0;
if (i>=0||i<size) {
e=data[i];
for(;i<size-1;i++){
data[i]=data[i+1];
}
if(data[size-1]!=0){
data[size-1]=0;
}
size--;
}else{
throw new IllegalArgumentException("删除元素位置不合理");
}
return e;
}
//判断线性表是否为空
public void isEmpty(){
if (size==0){
System.out.println("线性表为空");
}
System.out.println("当前线性表不为空");
}
//返回线性表的元素个数
public int getLength(){
return size;
}
//查找在线性表中的元素与给定值相等的序号
public int locateElem(int e){
for (int i = 0; i < size; i++) {
if (data[i] == e) {
return i;
}
}
return 0;
}
//获取线性表中第i个位置元素
public int getElem(int i){
if (i<0||i>size) {
throw new IllegalArgumentException("超出范围");
}
return data[i];
}
public String toString() {
return "List{" +
"存储的元素=" + Arrays.toString(data) +
", 线性表中的元素个数=" + size +
'}';
}
}
测试:
public class ListTest1 {
public static void main(String[] args) {
List list =new List();
for (int i=0;i<10;i++){
list.listInsert(i,i+1);
}
System.out.println(list);//List{存储的元素=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 线性表中的元素个数=10}
System.out.println(list.getElem(5));//6
System.out.println(list.locateElem(3));//2
System.out.println(list.getLength());//10
list.isEmpty();//当前线性表不为空
System.out.println(list.listDelet(4));//5
System.out.println(list.listDelet(4));//6
System.out.println(list);//List{存储的元素=[1, 2, 3, 4, 7, 8, 9, 10, 0, 0], 线性表中的元素个数=8}
list.listInsert(4,5);
System.out.println(list);//List{存储的元素=[1, 2, 3, 4, 5, 7, 8, 9, 10, 0], 线性表中的元素个数=9}
list.clearList();
System.out.println(list);//List{存储的元素=[0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 线性表中的元素个数=0}
list.destoryList();
System.out.println(list);//List{存储的元素=null, 线性表中的元素个数=0}
}
}
3.3.6线性表顺序存储结构的优缺点
- 优点
- 利用数据元素的存储位置表示线性表当中相邻数据元素之间的前后关系,这样无须为表示表中元素之间的逻辑关系而增加额外的存储空间
- 可以快速地存取表中的任意一个位置的元素
- 缺点
- 在插入、删除某一元素时,需要移动大量元素
- 当线性表长度变化较大时,难以确定存储空间的容量
- 浪费存储空间(造成存储空间的“碎片”)
- 存储空间碎片
存储空间的碎片指的是已分配的存储空间中存在一些未被使用的小块空间,这些小块空间的总和可能比实际需要的大很多,但是由于它们分散在不同的位置,无法组成足够大的连续空间来满足某些大块的内存申请,这种情况称为存储空间的碎片
3.4线性表的链式存储结构
3.4.1线性表链式存储结构定义
在链式结构中,除了要保存数据元素信息之外,还需要保存它后继元素的存储地址
我们把存储数据元素信息的域称为数据域;
把存储直接后继位置的域称为指指针域;
指针域中存储的信息称为指针或链;
这两部分信息组成数据元素ai的存储映像,称为结点
![](https://img-blog.csdnimg.cn/22c4b9d07899490885c1b4c754b6eb17.png)
3.4.1.1单链表
![](https://img-blog.csdnimg.cn/img_convert/722de072f8847814b7541a4ec810765a.png)
- n个结点链接成一个链表,即为线性表的链式存储结构,因为此链表的每个结点中只包含一个指针域,所以叫做单链表。
- 链表中的第一个结点的存储位置叫做首元结点
- 线性链表的最后一个结点指针为“空”(通常用NULL或“^”符号表示)
- 为了更加方便地对链表进行操作,会在单链表的第一个结点前附设一个结点,称为头结点。头结点的数据与可以不存储任何信息,也可以存储如线性表的长度等附加信息,但此结点不能计入链表长度值
- 指向链表中的头结点的指针就是头指针
头指针与头结点的异同:
头指针 | 头结点 |
---|---|
头指针是指链表指向第一个结点的指针,若是链表有头结点,则是指向头结点的指针 | 头结点是为了操作的统一和方便而建立的,放在第一元素的结点之前,其数据域一般无意义(可存放附加属性,例如存放链表的长度) |
头指针具有标识作用,所以常用头指针冠以链表的名字 | 有了头结点,对在第一元素结点前插入结点和删除第一结点,其操作与其他结点的操作就统一了 |
无论链表是否为空,头指针均不为空。头指针是链表的必要元素 | 头结点不一定是链表必要元素 |
3.4.1.2双向链表
结点有两个指针域的链表,称为双向链表
双向链表的优缺点
优点 | 缺点 |
---|---|
双向链表允许在链表中沿着两个方向遍历,即从头结点向后遍历,也可以从尾节点向前遍历。 | 双向链表需要额外的空间来存储每个结点的前驱和后继结点的引用。这意味着在存储相同数量的元素时,双向链表需要比单向链表更多的内存空间 |
双向链表可以更方便地实现一些操作,例如在链表中插入或删除一个结点时,可以更容易地找到该节点地前驱和后继结点 | 双向链表地实现相对单向链表要复杂一些,因为每个结点都需要同时存储前驱和后继结点地引用,这样会增加代码的复杂度和维护成本 |
双向链表可以更方便地实现反转链表地操作,因为每个结点都存储了其前驱和后继结点地引用 |
3.4.1.3循环链表
首尾相连的链表称为循环链表
3.4.1.4线性表的链式存储结构特点
- 用一组物理位置任意的存储单元来存放线性表的数据元素
- 这组存储单元既可以是连续的,也可以是不连续的,甚至是零散分布在内存中的任意位置上的
- 链表中元素的逻辑次序和物理次序不一定相同
- 访问时只能通过头指针进入链表,并通过每个结点的指针域依次向后顺序扫描其余结点,所以寻找第一个结点和最后一个结点所花费的时间不等
3.4.2单链表、双向链表、循环链表的比较
链表类型 | 数据结构 | 插入和删除操作 | 遍历操作 | 内存占用 |
---|---|---|---|---|
单链表 | 每个节点只有一个指针,指向下一个节点。 | 在任何位置插入或删除一个节点时,需要遍历整个链表,找到要插入或删除的位置,并更新相应的指针。 | 按照顺序遍历整个链表。 | 每个节点只有一个指针,相对于双向链表和循环链表需要更少的内存空间。 |
双向链表 | 每个节点有两个指针,分别指向前一个节点和下一个节点。 | 在任何位置插入或删除一个节点时,只需要更新前驱和后继节点的指针即可。 | 按照顺序遍历整个链表。 | 每个节点有两个指针,相对于单链表需要更多的内存空间。 |
循环链表 | 最后一个节点指向头节点,形成一个环形结构。 | 在任何位置插入或删除一个节点时,需要特别处理头节点和尾节点的情况。 | 按照顺序遍历整个链表,需要注意控制循环的次数,否则可能会进入死循环。 | 由于最后一个节点指向头节点,可以减少内存浪费,相对于单向链表和双向链表在某些情况下可以更加高效地利用内存空间。 |
3.4.3单链表的定义及操作
完整代码(点击此处查看简要分析)
定义一个结点类
public class ListNode {
int val; //数据域
ListNode next;//指针域,用来指向下一个结点
//构造方法
public ListNode(){}
public ListNode(int val){
this.val=val;
this.next=null;
}
}
定义单链表类LinkedList
public class LinkedList {
private ListNode firstNode;//头结点
private int length;//链表长度,用于存放在头结点的数据域中
public void setLength(int length) {
this.length = length;
}
public LinkedList() {
firstNode=new ListNode(length);
}
//判断此链表是否为空
//由于此链表含有头节点,只需要检查头结点的next属性是否为空即可
//链表还定义了一个属性length,也可以利用长度判断
public void isEmpty(){
if (firstNode.next!=null){
System.out.println("当前链表不为空");
}
System.out.println("当前链表为空");
}
//单链表的清空
/* 思路:
1.首先确认程序不再需要访问该单链表中的任何结点
2.遍历单链表,将每个结点的指针域设置为null,以避免出现悬挂指针的情况
3.在释放单链表中的结点所占用的内存空间时,使用Java中的垃圾回收机制来回收结点的内存空间
4.清空单链表之后,应该将头结点的指针设置为null,表示链表已经清空,最后将链表长度设置为0
*/
public void clearList(){
ListNode curr = firstNode.next;
while(curr!=null){
ListNode next = curr.next;
curr.next=null;
curr=next;
}
firstNode.next=null;
length=0;
}
//求链表的表长
public int getLength(){
return length;
}
//取单链表中第i个元素的内容
/*思路:
判断给的位置是否合法
从链表的头指针出发,顺着链域next逐个结点往下搜索,直至搜索到第i个结点为止
*/
public int getElem(int index){
if(index<0||index>length){
throw new IllegalArgumentException("超出范围");
}
ListNode curr=firstNode;
for(int i=0;i<index;i++){
curr = curr.next;
}
return curr.val;
}
//根据指定数据获取该数据所在的位置序号
public int getIndex(int data){
int index=1;
ListNode curr = firstNode.next;
while (curr!=null){
if (curr.val==data){
return index;
}
curr=curr.next;
index++;
}
return -1;
}
//根据指定数据获取到该数据所在的结点
public ListNode getAddress(int data){
ListNode curr = firstNode.next;
while(curr!=null){
if (curr.val==data){
return curr;
}
curr=curr.next;
}
return null;
}
//在第i个结点前插入新结点
public void insertElem(int index,ListNode listNode){
if (index<1||index>length){
throw new IllegalArgumentException("超出范围");
}
ListNode curr =firstNode;
ListNode next = firstNode.next;
if (index==1){
curr.next=listNode;
listNode.next=next;
}else {
for (int i=0;i<index-1;i++){
curr=curr.next;
next=next.next;
}
curr.next=listNode;
listNode.next=next;
}
length++;
}
//删除第i个结点
public void deletElem(int index){
if (index<0||index>length){
throw new IllegalArgumentException("超出范围");
}
ListNode curr = firstNode.next;
ListNode prev=firstNode;
if (index == 1) {
firstNode.next = curr.next;
length--;
} else {
int i = 0;
while (curr != null && i < index) {
if (i == index - 1) {
prev.next = curr.next;
length--;
break;
}
prev = curr;
curr = curr.next;
i++;
}
}
}
//头插法插入数据
public void inserAtHead(ListNode listNode){
listNode.next=firstNode.next;
firstNode.next=listNode;
length++;
}
//尾插法插入数据
public void insertAtEnd(ListNode listNode){
if (firstNode.next == null) {
firstNode.next = listNode;
} else {
ListNode prev = firstNode.next;
while (prev.next != null) {
prev = prev.next;
}
prev.next = listNode;
}
length++;
}
@Override
public String toString() {
return "LinkedList{" +
firstNode +
",链表长度=" + length +
'}';
}
}
测试
public class LinkedListTest {
public static void main(String[] args) {
LinkedList linkedList=new LinkedList();
ListNode listNode1=new ListNode(1);
ListNode listNode2=new ListNode(2);
linkedList.inserAtHead(listNode1);
//使用头插法插入数据
linkedList.inserAtHead(listNode2);
System.out.println(linkedList);//LinkedList{ 头结点=ListNode{val=0, next=ListNode{val=2, next=ListNode{val=1, next=null}}},链表长度=2}
//采用尾插法插入数据
ListNode listNode3=new ListNode(3);
linkedList.insertAtEnd(listNode3);
System.out.println(linkedList);//LinkedList{ 头结点=ListNode{val=0, next=ListNode{val=2, next=ListNode{val=1, next=ListNode{val=3, next=null}}}},链表长度=3}
//在第3个结点前插入新结点
ListNode listNode4=new ListNode(4);
linkedList.insertElem(3,listNode4);
System.out.println(linkedList);//LinkedList{ListNode{val=0, next=ListNode{val=2, next=ListNode{val=1, next=ListNode{val=4, next=ListNode{val=3, next=null}}}}},链表长度=4}
//删除第2个结点
linkedList.deletElem(2);
System.out.println(linkedList);//LinkedList{ListNode{val=0, next=ListNode{val=2, next=ListNode{val=1, next=ListNode{val=3, next=null}}}},链表长度=3}
//根据指定数据获取所在结点
System.out.println(linkedList.getAddress(1));//ListNode{val=1, next=ListNode{val=3, next=null}}
//根据指定数据获取位置序号
System.out.println(linkedList.getIndex(1));//2
//取第2个元素的内容
System.out.println(linkedList.getElem(2));//1
//获取链表长度
System.out.println(linkedList.getLength());//3
//清空单链表
linkedList.clearList();
System.out.println(linkedList);//LinkedList{ListNode{val=0, next=null},链表长度=0}
}
}
🍗初始化
由于定义的是一个单链表,首先先定义一个结点类,属性含有数据域与指针域,随后创建一个有参构造方法来使数据存放到数据域当中
public class ListNode {
int val; //数据域
ListNode next;//指针域,用来指向下一个结点
//构造方法
public ListNode(){}
public ListNode(int val){
this.val=val;
this.next=null;
}
@Override
public String toString() {
return "ListNode{" +
"val=" + val +
", next=" + next +
'}';
}
}
此链表在创建时就含有头结点,用来存放链表长度,故定义单链表时,属性含有头结点对象和长度
private ListNode firstNode;//头结点
private int length;//链表长度,用于存放在头结点的数据域中
public void setLength(int length) {
this.length = length;
}
public LinkedList() {
firstNode=new ListNode(length);
}
🍖在第i个结点前插入新结点
- 首先我们应该先判断一下给的位置是否合法,不能小于1或者是大于单链表长度
- 定义两个指针curr和next,分别指向链表的元结点与首元节点
- 判断要插入的位置
- 如果要插入的位置是首元结点,则将元结点的next指针指向新结点,新结点的next指针指向原首元结点
- 如果插入的位置不是首元节点,则循环遍历链表,直到找到要插入的结点的前一个结点位置,将前一个结点的next指针指向新结点,新结点的next指针指向原结点位置
public void insertElem(int index,ListNode listNode){
if (index<1||index>length){
throw new IllegalArgumentException("超出范围");
}
ListNode prev =firstNode;
ListNode next = firstNode.next;
if (index==1){
prev.next=listNode;
listNode.next=next;
}else {
for (int i=0;i<index-1;i++){
prev=prev.next;
next=next.next;
}
prev.next=listNode;
listNode.next=next;
}
length++;
}
🍚删除第i个结点
- 首先判断删除位置1是否超出链表范围,如果超出抛出异常
- 定义两个指针curr和prev,分别指向链表的首元节点和元结点
- 判断删除的位置
- 如果删除位置是首元结点,则将元结点的next指针指向原第二个结点,链表长度减一
- 如果删除位置不是首元结点,这循环遍历链表,直到找到要删除的结点位置,将prev的指针指向要删除的结点的下一结点,链表长度减一
public void deletElem(int index){
if (index<0||index>length){
throw new IllegalArgumentException("超出范围");
}
ListNode curr = firstNode.next;
ListNode prev=firstNode;
if (index == 1) {
firstNode.next = curr.next;
length--;
} else {
int i = 0;
while (curr != null && i < index) {
if (i == index - 1) {
prev.next = curr.next;
length--;
break;
}
prev = curr;
curr = curr.next;
i++;
}
}
}
3.4.4双向链表的定义及操作
🍐在指定位置插入一个结点
- 首先我们应该判断一下给定的位置是否合法,要求给的数要小于0大于链表长度
if (index < 0 || index > length) {
throw new IndexOutOfBoundsException("超出范围");
}
- 接着我们需要做的是分析一下插入位置,可以分为三种情况
-
第一种:插入到第一个位置
- 由于在双向链表中定义了一个首元结点head(初始化时为null),故当插入一个新结点时,新结点的next指针指向head
- 当首元结点不为空时,这个时候相当于链表含有元素,将head结点的前驱指针指向新结点
- 当首元结点为空时,相当于向一个空的链表中插入一个结点,那么这个结点既是首元结点,也是尾结点
if (index == 0) { if (head == null) { head=node; end = node; } else { head.prev = node; node.next=head; head=node; } length++; }
-
第二种:插入到最后一个位置
- 在定义双向链表时就已经定义了尾结点end(初始化时为null),故当新结点插入到最后一个位置时,新结点的前驱指针指向end结点
- 如果尾结点不为空的话,将尾结点的后继指针指向新结点
- 如果尾结点为空,相当于此链表没有元素,故插入的结点既是首元结点也是尾结点
else if (index == length) { if (end==null){ head=node; end=node; }else{ node.prev = end; end.next = node; end=node; } length++; }
-
第三种:插入到除了第一个位置和最后一个位置的其他位置
- 由于已经定义的有首元结点,通过循环遍历,找到要插入位置的前一个结点,随后将前一个结点的后继结点的前驱指针指向新结点,前一个结点的后继指针指向新结点
else { DoublyLinkedListNode<T> curr = head; for (int i = 0; i < index; i++) { curr = curr.next; } curr.prev.next=node; node.prev = curr.prev; curr.prev = node; node.next=curr; length++; }
-
🍑反转链表
原理:当我们需要反转一个双向链表,需要将每一个结点的prev和next指针互换位置
。这样,每个结点的前一个结点变成了它的后一个结点,后一个结点变成了它的前一个结点,整个链表的方向也就被反转了.
简单举一个例子
假设含有一个包含4个结点的双向链表,每个结点的值分别为1,2,3,4,链表的首元结点为1,尾结点为4:
具体实现反转:
1.初始化一个指针node,指向null
2.初始化一个指针curr,指向双向链表的首元结点
3.遍历链表,每次将curr的prev和next指针互换位置,然后将curr向后移一位,直到curr为空为止
4.将链表的首元结点指向原来的尾结点,将链表的尾结点指向原来的头结点
public void reverse(){
DoublyLinkedListNode<T> curr=head;
DoublyLinkedListNode<T> node=null;
while(curr!=null){
node=curr.prev;
curr.prev=curr.next;
curr.next=node;
curr=curr.prev;
}
node=head;
head=end;
end=head;
}
🍒注意
在代码中,要时刻注意指针的变化,稍不留神就会出错,另外,在牵扯到首元结点时,要先定义一个新结点,例如DoublyLinkedListNode<T> curr = head;这是为了在之后的操作中能够不改变首元结点head的值
完整代码
定义一个结点类
package doubledanlianbiao;
public class DoublyLinkedListNode <T>{
public T data;//结点数据
public DoublyLinkedListNode<T> prev;//指向前驱结点的指针
public DoublyLinkedListNode<T> next;//指向后继结点的指针
public DoublyLinkedListNode() {
}
public DoublyLinkedListNode(T data) {
this.data = data;
this.prev = null;
this.next = null;
}
}
定义一个循环链表
package doubledanlianbiao;
public class DoublyLinkedList<T> {
private DoublyLinkedListNode<T> head;//首元结点
private DoublyLinkedListNode<T> end;//尾结点
private int length;
public DoublyLinkedList() {
head = null;
end = null;
length = 0;
}
public int getLength() {
return length;
}
//在指定位置插入一个结点
public void insert(int index, T data) {
if (index < 0 || index > length) {
throw new IndexOutOfBoundsException("超出范围");
}
DoublyLinkedListNode<T> node=new DoublyLinkedListNode<T>(data);
if (index == 0) {
if (head == null) {
head=node;
end = node;
} else {
head.prev = node;
node.next=head;
head=node;
}
length++;
} else if (index == length) {
if (end==null){
head=node;
end=node;
}else{
node.prev = end;
end.next = node;
end=node;
}
length++;
} else {
DoublyLinkedListNode<T> curr = head;
for (int i = 0; i < index; i++) {
curr = curr.next;
}
curr.prev.next=node;
node.prev = curr.prev;
curr.prev = node;
node.next=curr;
length++;
}
}
//获取指定位置的数据
public T getElem(int index) {
if (index < 0 || index >= length) {
throw new IndexOutOfBoundsException("超出范围");
}
DoublyLinkedListNode<T> cur = head;
for (int i = 0; i < index; i++) {
cur = cur.next;
}
return cur.data;
}
//删除指定位置的结点
public void deleByIndex(int index) {
if (index < 0 || index >= length) {
throw new IndexOutOfBoundsException("超出范围");
}
if (index == 0) {
head = head.next;
if (head != null) {
head.prev = null;
} else {
end = null;
}
length--;
} else if (index == length - 1) {
end = end.prev;
end.next = null;
length--;
} else {
DoublyLinkedListNode<T> curr = head;
for (int i = 1; i < index; i++) {
curr = curr.next;
}
curr.next = curr.next.next;
curr.next.prev = curr;
length--;
}
}
//遍历链表输出所有结点中的数据
public void traverse(){
DoublyLinkedListNode<T> curr=head;
StringBuilder sb=new StringBuilder();
while (curr!=null){
sb.append(curr.data).append(",");
curr=curr.next;
}
String str=sb.toString();
if (str.endsWith(",")){
str=str.substring(0,str.length()-1);
}
System.out.println(str);
}
//反转链表
public void reverse(){
DoublyLinkedListNode<T> curr=head;
DoublyLinkedListNode<T> node=null;
while(curr!=null){
node=curr.prev;
curr.prev=curr.next;
curr.next=node;
curr=curr.prev;
}
node=head;
head=end;
end=head;
}
}
测试
package doubledanlianbiao;
public class test {
public static void main(String[] args) {
DoublyLinkedList<String> dbList=new DoublyLinkedList<String>();
dbList.insert(0,"000");
System.out.println(dbList.getLength());//1
dbList.insert(1,"111");
dbList.insert(1,"222");
//遍历所有数据
dbList.traverse();//000,222,111
//反转链表
dbList.reverse();
dbList.traverse();//111,222,000
//删除指定位置结点
dbList.deleByIndex(1);
dbList.traverse();//111,000
}
}
3.4.5循环链表的定义及操作
循环链表相对于单链表和双向链表的区别只在于尾结点的后继指针是否指向首元结点,故大多操作都类似,这里只对反转链表进行分析。
🍓反转链表
- 循环链表的反转操作实际与前面的双向链表区别也不是很大,主要表现在反转后头结点的位置上
- 通过do···while遍历循环,改变当前结点的前后指针,当达到循环截止条件时,curr指针指向了原首元结点
- 将head指针指向curr的后继结点,head的前驱指针指向curr
public void reverse(){
Node<T> curr=head;
Node<T> next=null;
Node<T> prev=null;
do{
next=curr.next;
prev=curr.prev;
curr.next=prev;
curr.prev=next;
curr=next;
}while(curr!=head);
head=curr.next;
head.prev=curr;
}
完整代码
定义节点类
package circle;
public class Node<T> {
public T data;
public Node<T> next;
public Node<T> prev;
public Node() {
}
public Node(T data) {
this.data = data;
this.next = null;
this.prev=null;
}
@Override
public String toString() {
return "data=" + data ;
}
}
定义循环双向链表
package circle;
import doubledanlianbiao.DoublyLinkedListNode;
public class CircleList<T>
{
private Node<T> head;
private int length;
public CircleList() {
this.head = null;
this.length = 0;
}
//根据索引获取结点
public Node<T> getElemByIndex(int index){
if (index < 0 || index > length) {
throw new IndexOutOfBoundsException("超出范围");
}
Node<T> node=head;
for (int i=0;i<index;i++){
node=node.next;
}
return node;
}
//在指定位置插入结点
public void insert(int index,T data){
if (index < 0 || index > length) {
throw new IndexOutOfBoundsException("超出范围");
}
Node<T> node=new Node<>(data);
if (index==length){
//插入位置为0的情况
if (head==null){
head=node;
head.prev=head;
head.next=head;
}
else{
Node<T> end=head.prev;
end.next=node;
node.prev=end;
node.next=head;
head.prev=node;
}
length++;
}else{
Node<T> curr=head;
for (int i=0;i<index;i++){
curr=curr.next;
}
curr.prev.next=node;
node.prev=curr.prev;
node.next=curr;
curr.prev=node;
length++;
}
}
//在指定位置删除结点
public void delet(int index){
if (index < 0 || index > length) {
throw new IndexOutOfBoundsException("超出范围");
}
Node<T> curr=head;
for(int i=0;i<index;i++){
curr=curr.next;
}
if (curr==head){
head=curr.next;
}
curr.prev.next=curr.next;
curr.next.prev=curr.prev;
length--;
}
//反转链表
public void reverse(){
Node<T> curr=head;
Node<T> next=null;
Node<T> prev=null;
do{
next=curr.next;
prev=curr.prev;
curr.next=prev;
curr.prev=next;
curr=next;
}while(curr!=head);
head=curr.next;
head.prev=curr;
}
//遍历链表
public void traverse(){
Node<T> curr=head;
StringBuilder sb=new StringBuilder();
do{
sb.append(curr.data).append(",");
curr=curr.next;
}while(curr!=head);
String str=sb.toString();
if (str.endsWith(",")){
str=str.substring(0,str.length()-1);
}
System.out.println(str);
}
}
测试
package circle;
public class test1 {
public static void main(String[] args) {
CircleList<String> list=new CircleList<>();
list.insert(0,"1");
list.insert(1,"2");
list.traverse();//1,2
System.out.println(list.getElemByIndex(1));//2
list.insert(2,"3");
list.insert(3,"4");
list.traverse();//1,2,3,4
//反转操作
list.reverse();
list.traverse();//4,3,2,1
}
}
3.5顺序存储与链式存储的比较
特点 | 顺序表 | 链表 |
---|---|---|
存储方式 | 一段连续的存储空间 | 分散的存储空间,每个节点包含数据和指向下一个节点的指针 |
插入和删除操作 | 需要移动后面的元素,时间复杂度为O(n) | 只需要改变节点的指针,时间复杂度为O(1) |
访问操作 | 可以通过下标直接访问元素,时间复杂度为O(1) | 需要从头节点开始遍历,时间复杂度为O(n) |
空间占用 | 需要预先分配一段连续的内存空间,占用固定空间 | 不需要预先分配空间,每个节点只占用必要的空间,更加灵活 |
容量限制 | 容量受到预分配的内存空间大小的限制 | 容量不受限制,只受计算机内存大小的限制 |
![](https://img-blog.csdnimg.cn/c5629cb4fe9c4ed9924e72668cbf083b.png)