链表
线性表的定义
线性表(List):零个或者多个具有相同类型的数据元素的有限序列。
若将线性表记为(a1,…,ai-1,ai,ai+1,…,an)的话。则表中ai-1领先于ai,而ai领先于ai+1。故称ai-1是ai的直接前驱元素,ai+1是ai的直接后继元素。如图所示。
n=0时候,称为空表。
同时,在较为复杂的线性表中,一个数据元素可以由若干个数据项组成。就如下表一样
他们必须要有相同的数据类型。
同时,再聊一聊线性表的抽象数据类型。个人理解,就是对线性表的基本操作。
主要操作
~删除:移除并返回表中指定位置的元素
~插入:插入一个元素到表中
辅助操作
~删除表:清空表中所有元素
~计数:返回表中的元素个数
~查找:寻找从表表尾开始的第n个结点(node)
不多BB,先上线性表的两种结构的分类图
这里值得注意一下:线性表 != 链表, 或者说的是 链表属于线性表的一种分类。
线性表的顺序存储结构(数组)
指的是用一段地址连续的存储单元依次存储线性表的数据元素。通常使用数组来实现。
数组长度是固定的,线性表的长度不固定,线性表的当前长度不得大于数组长度,否则会报错。或者动态扩容,但是这样操作会有性能上的损耗。
为了代码方便,这里直接将数据类型定义为int类型
插入操作
//插入操作
删除操作
//删除操作
查找操作
//查找操作
好了,写到这里,因为顺序存储结构两个元素的存储位置具有“相邻”的邻居关系,所以需要链表来解决。
该讲讲接下来的重点:链表。
单向链表
链表同行指的是单向链表,包含多个结点,每个结点都有指向后继元素的next指针(下一个指针)。最后一个结点指针值为Null,代表着链表的结束。
链表的类型声明代码:
public class ListNode{
private int data;
private ListNode next;
public ListNode(int data){
this.data = data;
}
//setter,getter方法
}
遍历
沿着指针遍历,遍历时候显示结点的内容,next指针值为null时候则结束遍历。
下面代码为统计个数。
int ListLength(ListNode headNode){
int length = 0;
ListNode currentNode = headNode;
while(currentNode!= null){
length++;
System.out.pritln("data>>>>>>"+currentNode.getData());
currentNode = currentNode.getNext();
}
}
@Test
public void test(){
ListNode listNode = new ListNode();
ListNode listNode2 = new ListNode();
listNode.setData(111);
listNode2.setData(222);
listNode.setNext(listNode2);
listNode2.setNext(null);
System.out.println("listLength>>>>>>>>>>>>>"+ListLength(listNode));
}
// 输出结果为
//data>>>>111
//data>>>>222
//listLgenth>>>>>>2
插入操作
思路如下
-
若在单向链表的开头插入结点
只需要修改一个next指针(新结点的next指针),即可完成。 -
若在单向链表的结尾插入结点
修改两个指针(最后一个结点的next指针和新结点的next指针)
-
若在单向链表的中间插入结点
修改两个指针
ListNode InsertInLinkedList(ListNode headNode, ListNode nodeToInsert, int position){
//链表为空,直接插入
if(headNode==null){
return nodeToInsert;
}
int size = ListLength(headNode);
//判断插入位置合法与否
if(position>size+1 ||position<1){
System.out.println("插入位置不合法,合法插入位置是 1到"+(size+1));
return headNode;
}
//在链表开头插入
if(position ==1){
nodeToInsert.setNext(headNode);
return nodeToInsert;
}else {
//在中间或者末尾插入
ListNode previousNode = headNode;
int count = 1;
//知道位置在哪里插入,获得到插入位置的前一个结点
while (count< position-1){
previousNode = previousNode.getNext();
count++;
}
//获得插入结点的后一个结点
ListNode currentNode = previousNode.getNext();
//需要插入的结点的下一个结点自然而然就是currentNode
nodeToInsert.setNext(currentNode);
//同理
previousNode.setNext(nodeToInsert);
}
return headNode;
}
删除操作
ListNode deleteFromLinkedList(ListNode headNode, int position){
int size = ListLength(headNode);
//判断删除位置合法与否
if(position>size ||position<1){
System.out.println("插入位置不合法,合法插入位置是 1到"+(size));
return headNode;
}
//第一个删除
if(position ==1){
ListNode currentNode = headNode.getNext();
headNode = null;
return currentNode;
}else{
//结尾或中间删除
ListNode preNode = headNode;
int count = 1;
while (count < position -1 ){
preNode = preNode.getNext();
count++;
}
//这个就是被删的了
ListNode currentNode = preNode.getNext();
preNode.setNext(currentNode.getNext());
currentNode=null;
}
return headNode;
}
@Test
public void testDeleteOne(){
ListNode listNode1 = new ListNode();
ListNode listNode2 = new ListNode();
ListNode listNode3 = new ListNode();
ListNode listNode4 = new ListNode();
listNode1.setData(11111);
listNode2.setData(22222);
listNode3.setData(33333);
listNode4.setData(44444);
listNode1.setNext(listNode2);
listNode2.setNext(listNode3);
listNode3.setNext(listNode4);
listNode4.setNext(null);
// System.out.println(deleteFromLinkedList(listNode1, 1));
// System.out.println(">>>>>>>>>>>");
System.out.println(listNode1);
System.out.println(deleteFromLinkedList(listNode1, 2));
System.out.println(">>>>>>>>>>>>>");
}
查找操作
ListNode findNode(int position,ListNode headNode){
if(headNode==null){
System.out.println("链表为空");
return null;
}
int size = ListLength(headNode);
if(position<1 || position>size){
System.out.println("位置不合法,合法位置是 1到"+(size));
return null;
}
int count =1;
ListNode currentNode = headNode;
while(count< position){
currentNode = currentNode.getNext();
count++;
}
return currentNode;
}
@Test
public void testFindNode(){
ListNode listNode1 = new ListNode();
ListNode listNode2 = new ListNode();
ListNode listNode3 = new ListNode();
ListNode listNode4 = new ListNode();
listNode1.setData(11111);
listNode2.setData(22222);
listNode3.setData(33333);
listNode4.setData(44444);
listNode1.setNext(listNode2);
listNode2.setNext(listNode3);
listNode3.setNext(listNode4);
listNode4.setNext(null);
System.out.println(findNode(1,listNode1));
System.out.println(findNode(2,listNode1));
}
双向链表
故名思议,双向链表,就是双向的链表。
他的主要优点是:
- 可以从两个方向进行操作,不同于单向列表那样只有获得前驱结点的指针,才能删除该结点。
主要的缺点也是有的:
- 每个结点要多个额外的指针,需要更多的空间。
- 插入删除需要更多的操作
类型声明代码
public class DLLNode{
private int data;
private DLLNode next;
private DLLNode previous;
private DLLNode(int data){
this.data = data;
}
@Override
public String toString() {
return "DLLNode{" +
"data=" + data +
", next=" + next +
'}';
}
public int getData() {
return data;
}
public void setData(int data) {
this.data = data;
}
public DLLNode getNext() {
return next;
}
public void setNext(DLLNode next) {
this.next = next;
}
public DLLNode getPrevious() {
return previous;
}
public void setPrevious(DLLNode previous) {
this.previous = previous;
}
}
遍历操作
int getDLLNodeLength(DLLNode headNode){
int length = 0;
DLLNode currentNode = headNode;
while(currentNode!=null){
length++;
currentNode = currentNode.getNext();
}
return length;
}
@Test
public void testDLLNOdeLength(){
DLLNode node1 = new DLLNode(111);
DLLNode node2 = new DLLNode(222);
DLLNode node3 = new DLLNode(333);
DLLNode node4 = new DLLNode(444);
node1.setPrevious(null);
node1.setNext(node2);
node2.setPrevious(node1);
node2.setNext(node3);
node3.setPrevious(node2);
node3.setNext(node4);
node4.setPrevious(node3);
node4.setNext(null);
System.out.println(getDLLNodeLength(node1));//4
}
插入操作
DLLNode DLLNodeInsert(DLLNode headNode,DLLNode needToInsertNode, int position){
if(headNode ==null){
return needToInsertNode;
}
int size = getDLLNodeLength(headNode);
System.out.println("链表长度="+size);
if(size+1<position || position<1){
System.out.println("插入位置错误,可插入的位置为1到"+(size+1));
return headNode;
}
if(position==1){
needToInsertNode.setPrevious(null);
needToInsertNode.setNext(headNode);
headNode.setPrevious(needToInsertNode);
return needToInsertNode;
}else {
DLLNode preNode = headNode;
int count =1 ;
while(count<position-1){
preNode = preNode.getNext();
count++;
}
DLLNode currentNode = preNode.getNext();
needToInsertNode.setNext(currentNode);
if(currentNode!=null){
currentNode.setPrevious(needToInsertNode);
}
preNode.setNext(needToInsertNode);
needToInsertNode.setPrevious(preNode);
}
return headNode;
}
删除操作
DLLNode DLLNodeDelete(DLLNode headNode,int position){
int size = getDLLNodeLength(headNode);
if(position>size || position<1){
System.out.println("Invaild size,the right size should be 1 to "+size);
return headNode;
}
if(position==1){
DLLNode currentNode = headNode.getNext();
currentNode.setPrevious(null);
headNode= null;
return currentNode;
}else {
DLLNode previousNode = headNode;
int count = 1;
if(count<position-1){
count++;
previousNode =previousNode.getNext();
}
//得出被删除的结点
DLLNode currentNode = previousNode.getNext();
//如果被删除的结点不是最后一个的话,即是中间
if(currentNode.getNext()!=null){
//让被删除结点的前驱结点 指向 被删除结点的后驱结点
previousNode.setNext(currentNode.getNext());
//被删除结点的后驱结点也指向被删除的前驱结点
currentNode.getNext().setPrevious(previousNode);
currentNode=null;
}else {
previousNode.setNext(null);
currentNode=null;
}
}
return headNode;
}
循环链表
直接上优缺点吧:
- 优点,遍历灵活,没有null,直接从任意结点遍历都可
- 缺点,查找慢
结构代码又如单向链表一样
public class CircularList{
private int data;
private CircularList next;
@Override
public String toString() {
return "CircularList{" +
"data=" + data +
", next=" + next +
'}';
}
public int getData() {
return data;
}
public void setData(int data) {
this.data = data;
}
public CircularList getNext() {
return next;
}
public void setNext(CircularList next) {
this.next = next;
}
}
遍历操作
int findCircularListLength(CircularList headNode){
int length = 0;
CircularList currentNode = headNode;
while(currentNode!=null){
length++;
currentNode = currentNode.getNext();
if(currentNode==headNode){
break;
}
}
return length;
}
插入操作
void insertIntoEndCLL(CircularList headNode,CircularList needToInsertNode){
CircularList currentNode = headNode;
while(currentNode.getNext()!=headNode){
currentNode.setNext(currentNode.getNext());
}
//容易理解,当currentNode.getNext==headNode时,currentNode就是尾结点了
needToInsertNode.setNext(needToInsertNode);
if(headNode==null){
headNode =needToInsertNode;
}else {
needToInsertNode.setNext(headNode);
currentNode.setNext(needToInsertNode);
}
}
void insertIntoBeginCLL(CircularList headNode,CircularList needToInsertNode){
CircularList currentNode = headNode;
while(currentNode.getNext()!=headNode){
currentNode.setNext(currentNode.getNext());
}
needToInsertNode.setNext(needToInsertNode);
if(headNode==null){
headNode =needToInsertNode;
}else {
needToInsertNode.setNext(headNode);
currentNode.setNext(needToInsertNode);
//直接把头结点变成需要插入的即可了。。。
headNode=needToInsertNode
}
删除操作
void deleteLastNodeFromCircularList(CircularList headNode){
CircularList temp = headNode;
CircularList currentNode = headNode;
if(headNode==null){
System.out.println("链表为空");
return;
}
while (currentNode.getNext()!=headNode){
temp = currentNode;
currentNode = currentNode.getNext();
}
temp.setNext(headNode);
currentNode = null;
return;
}
void deleteFrontNodeFromDLL(CircularList headNode){
CircularList temp = headNode;
CircularList current = headNode;
if(headNode==null){
System.out.println("List Empty");
return;
}
while (current.getNext()!=headNode)
current.setNext(current.getNext());
current.setNext(headNode.getNext());
headNode = headNode.getNext();
temp = null;
return;
}
总结
顺序存储(数组)的优点:
- 无须为表中元素之间的逻辑而增加额外的存储空间。
- 可以快速的存取表中任何一位置的元素(查找快,常数时间)。
顺序存储(数组)的缺点:
- 插入和删除操作需要移动大量元素。
- 线性表长度变化较大的时候,难以确定存储空间的容量。
- 造成存储空间的“碎片”。
链式存储(链表)的优点:
- 可以在常数时间内扩展,不同于数组的麻烦扩展(新建数组,把原数组的元素复制进去,新建的的数组大小也是一个迷…)。链表可以简单,仅仅多分配一个元素的存储空间。
链式存储(链表)的缺点:
- 访问单个元素时候过慢,若元素在第一个,则时间复杂度为O(1),若在末尾,则为O(n)。
- 而数组因为是连续的内存块,任何数组元素都是物理相连的,故比他快。
- 额外指针引用浪费内存。
附上一个链表,数组,动态数组的比较
本人学习的理解笔记,如有错误!不吝赐教,请留言,谢谢你!