## 单链表是一种基本且常用的数据结构, 它通过指针将一系列节点(元素)串联起来。 每个节点包含两个主要部分:数据域和指针域。 ##
1. 结构定义
在C语言中,单链表的节点通常可以这样定义:
typedef struct LNode { int data; // 数据域,存储具体的数据内容 struct LNode *next; // 指针域,指向下一个节点的地址 } LNode, *LinkList;
这里的
在JAVA语言中,单链表的节点通常可以这样定义:
public static class list_node{ public int val; public list_node next;//类似于指针指向下一个元素节点的地址 public list_node(int val){ this.val=val;//外部需要传递的元素值 } } public list_node head;//指定头部
这里的的head
2. 链表特性
- 动态性:链表的长度可以在程序运行过程中任意改变,可以根据需要添加或删除节点。
- 非连续存储:链表中的各个节点在内存中不必是连续存放的,相邻节点之间通过指针相连。
- 顺序访问:只能从一个节点通过其
next 指针找到下一个节点,不能随机访问。
3. 常见操作
- 创建链表:初始化头结点,然后逐步添加新节点
- `public void create_list(){ list_node node1=new list_node(11); list_node node2=new list_node(13); list_node node3=new list_node(11); list_node node4=new list_node(12); list_node node5=new list_node(20); node1.next=node2; node2.next=node3; node3.next=node4; node4.next=node5; this.head=node1;//指定头部在第一个位置 }`
- 插入节点:
- 头插法:在链表头部插入新的节点。
- 中间插入:在指定位置后插入新节点。
- 尾插法:在链表尾部插入新的节点。
- 删除节点:根据给定值或节点位置删除相应节点。
- 遍历链表:通过指针逐个访问每个节点。
- 查找节点:按照给定条件搜索链表中的节点是否存在。
- 反转链表:修改节点间的连接方向以实现链表反转。
- 接口实现:
public interface Ilist_single { //定义接口 public void addFirst(int data); public void addLast(int data); public void addIndex(int index, int data); public boolean contains(int key); public void remove(int key); public void removeAllKey(int key); public int size(); public void clear(); public void display(); }
`
4. 核心代码实现
package override_single_linked_list; import link_list.my_link_list; import link_list.yi_chang; public class single_linked_list implements Ilist_single { public static class list_node{ public int val; public list_node next;//类似于指针指向下一个元素节点 public list_node(int val){ this.val=val;//外部需要传递的元素值 } } public list_node head;//指定头部 public void create_list(){ list_node node1=new list_node(11); list_node node2=new list_node(13); list_node node3=new list_node(11); list_node node4=new list_node(12); list_node node5=new list_node(20); //指向关系 一个节点指向下一个节点 node1.next=node2; node2.next=node3; node3.next=node4; node4.next=node5; this.head=node1;//指定头部在第一个位置 } @Override public void addFirst(int data) { //头插法 list_node new_elments=new list_node(data);//定义一个将要插入的新元素 if(head==null){ new_elments.next=head; } else{ new_elments.next=head; this.head=new_elments; } } @Override public void addLast(int data) { //尾插法 list_node new_elments=new list_node(data); list_node cur=this.head; if(cur==null){ cur=new_elments;//new_elments:是节点的地址 } else{ while(cur.next!=null){ cur=cur.next;//一直往下遍历 } cur.next=new_elments; } } @Override public void addIndex(int index, int data) { //根据索引位置插入 //首先要检查index是否越界的问题 if(index<0||index>size()){ throw new yi_chang_("这是越界的异常"); } if(index==0){ addFirst(data); } if(index==size()){ addLast(data); } //这些情况都不是的话进行按索引位置插入 //需要找到前一个位置 //不能去找后一个位置 // 因为一旦后一个位置确定那么前一个位置无法定位 // 因为单链表顺序而下 不能回退 list_node prev=search_node(index); list_node new_lements=new list_node(data); new_lements.next=prev.next; prev.next=new_lements; } private list_node search_node(int index) { //找到index的前一个位置 list_node cur = this.head; int count = 0; while (count != index - 1) { cur = cur.next; count++; } return cur; //两种方式 // list_node cur=this.head; // while(index-1!=0){ // cur=cur.next; // index--; // } // return cur; // } } @Override public boolean contains(int key) { //查找是否包含关键字key key是否在链表中 list_node cur=this.head; while(cur!=null){ if(cur.val==key){ return true; } cur=cur.next; } return false; } @Override public void remove(int key) { //删除第一次出现key的节点 if(this.head==null){ return; } if(head.val==key){ head=head.next; return ; } //需要找到前一个的index list_node index_prev=search_node_index(key); if(index_prev==null){ return; } //定义一个要删除的元素 list_node del = new list_node(key); index_prev.next=del.next; } private list_node search_node_index(int key) { list_node cur=this.head; while(cur.next!=null){ if(cur.val==key){ return cur; } cur=cur.next; } return null; } @Override public void removeAllKey(int key) { //移除所有的包含key的节点 if(this.head==null){ return; } list_node prev=this.head; list_node cur=this.head; while(cur!=null){ if(cur.val==key){ prev.next=cur.next; cur=cur.next; }else{ prev=cur; cur=cur.next; } } if(head.val==key){ head=head.next; } } @Override public int size() { int count=0; list_node cur=this.head; while(cur!=null){ cur=cur.next; count++; } return count; } @Override public void clear() { //清盘 list_node cur=this.head; while(cur!=null){ cur.next=null; cur=cur.next; } this.head=null; } @Override public void display() { list_node cur=this.head; while(cur!=null){ System.out.println(cur.val); cur=cur.next; } } }
以上就是单链表的基本概念及其一些典型操作的简要说明。
**总的来说:
单链表的难点在于在特定位置进行插入:( public void addIndex(int index, int data) ;)
以及
移除全部包含特定值的节点:( public void removeAllKey(int key) ;),**
5.原理剖析
在单链表中,插入一个节点到特定位置的本质是通过遍历链表找到该位置,并修改相应节点的指针来实现新节点与原链表的有效连接。以下是一个图例解析:
假设我们有一个单链表如下所示,其中每个节点包含数据部分和指向下一个节点的指针(
1 -> 2 -> 3 -> 4 -> 5 ^ | head
现在我们要在第1个位置(从0开始计数,即在数值为1的节点之前)插入一个新的节点值为node。
本质上就是交换地址:
- 新的元素node的next==null的改为23的地址0x81(node.next=23.next)
2.将元素23所在的地址0x81改为node的地址0x65
(23.next=node)
相当于执行以下代码操作:
newNode.next = current.next; current.next = newNode;
这样,在单链表中的指定位置成功地插入了一个新节点X。
6.单链表的复杂度以及优缺点
单链表的数据结构具有以下的时间复杂度和空间复杂度,以及优缺点:
时间复杂度:
- 查找:在单链表中查找特定元素(线性查找)的时间复杂度是O(n),因为必须从头节点开始逐个遍历直到找到目标或者遍历完所有节点。
- 插入:在已知位置插入一个节点(比如在某个节点之后插入)的时间复杂度是O(1)(找到该位置的时间复杂度为O(n),但修改指针指向的操作是常数时间);而在有序链表中插入并保持有序,需要O(n)的时间复杂度(可能需要移动到正确的位置)。
- 删除:在已知位置删除一个节点的时间复杂度也是O(1)(找到节点后直接修改前后节点的指针),如果只知道待删除元素的值,则查找加删除操作总的时间复杂度是O(n)。
空间复杂度:
- 单链表的空间复杂度是O(n),其中n表示链表中的节点数量。每个节点都包含数据和指向下一个节点的指针。
优点:
- 动态性:可以方便地进行动态内存分配,不需要预先知道数据的大小,适合于频繁进行插入和删除操作的情况。
- 插入和删除效率高:对于非首尾节点,在不考虑查找的情况下,插入和删除操作仅需修改相邻节点的指针即可完成,时间复杂度较低。
- 空间利用率:由于链表节点可以存储在内存中任何地方,不需要连续空间,因此当内存碎片较大时,链表能更有效地利用内存资源。
缺点:
- 查找效率低:链表不适合需要快速随机访问的场景,无法像数组或顺序表那样通过下标直接访问。
- 需要额外存储空间:除了数据本身外,还需要额外存储指针信息,导致整体空间开销大于相同数据量的静态数组。
- 无序链表插入与删除虽快,但维持有序状态时,插入与删除的成本会显著增加。
- 不支持高效的随机访问和范围查询,对缓存不友好,连续内存访问性能差。
应用场景:
单链表常用于实现各种高级数据结构(如循环链表、双向链表、LRU缓存等)以及解决实际问题,如文件系统目录结构、数据库索引结构等。