1、前言
由于顺序表要求将元素存储在一块连续的空间中,因此对顺序表的元素进行插入,删除时需要对元素进行移动,这些操作会耗费大量的时间。所以就有了链式表的产生。链式表不要求使用连续的存储空间来存储元素,它是通过连接的方式,来将不连续的物理空间连接成一条链式的逻辑上是相邻的结构。对链式表进行插入,删除等操作就不需要移动元素了,这样大大提高了效率。接下来介绍单链表,它是链式表中最基本的一种结构。
2、概念
单链表是由于它的数据元素都含有一个连接点而得名。由于单链表除了含有数据信息之外还必须含有连接点信息,因此单链表的数据结构是复合类型。在单链表中,含有一个数据域,它用来存放当前结点的数据信息,含有一个指针域(如果在c语言中,可以用指针指向下一个元素,在java中则可以使用自包含的方式,保存下一个元素对象),用来存放下一节点的信息。单链表有且只有一个头节点,它的数据域是null,它的指针域指向第一个元素的地址,单链表也有且只有一个尾节点,它的数据域是尾节点的数据信息,指针域则是null,表示到了链表的末尾了。但一个链表的头节点的指针域是null的时候,表示该单链表是null的。
空表如下图,手画
:
![再见](http://static.blog.csdn.net/xheditor/xheditor_emot/default/bye.gif)
含有元素的表:
3、单链表的基本运算
下面定义基于单链表这种结构的基本运算,并随后用c,C++,java分别实现,注意,此后实现的操作每次添加的元素都处于表头之后,即总是作为第一个节点存在(表头不算节点)。
一、初始化单链表表头,取名init_linkList。
二、添加元素,取名insert_element。
三、获取指定位置的元素,取名get_element。
四、删除指定位置的元素,取名delete_element。
4、C语言实现单链表的基本运算
每个方法的逻辑在后面解释,如果看代码觉得逻辑混乱可以先看后头的逻辑解释。
/*
用C语言实现单链表的基本运算,这里实现的插入元素的顺序是倒叙的,和顺序表是相反的,即越后添加的元素在链表的越前面。
*/
#include <stdio.h>
#include <stdlib.h>
/*
声明单链表的数据结构,共有两个成员,数据访问域,指针访问域。
*/
struct node
{
int data;//这个可以是任意类型
struct node * next;//指向下一个节点
};
/*
声明单链表的基本运算
*/
struct node* init_linkList();
int insert_element(struct node* linkList, int x);
int delete_element(struct node* linkList, int position);
int get_element(struct node *linkList, int position);
int main(void)
{
struct node *linkList = init_linkList();
for (int i = 1; i <= 50; i++)
{
insert_element(linkList, i);
}
printf("打印1-50:\n");
for (int i = 1; i <= 50; i++)
{
printf("%d ", get_element(linkList, i));
if (i % 10 == 0)
printf("\n");
}
printf("\n");
printf("试图删除越界元素:");
delete_element(linkList, 60);//删除越界元素
delete_element(linkList, 1);//删除首节点
printf("50被删除掉了\n");
for (int i = 1; i <= 49; i++)
{
printf("%d ", get_element(linkList, i));
if (i % 10 == 0)
printf("\n");
}
printf("\n");
system("pause");
return 0;
}
/*
初始化一个空的单链表
*/
struct node* init_linkList()
{
struct node* header;//表头
header = (struct node*)malloc(sizeof(struct node));//给表头申请空间
header->data = -1;//表头数据为null
header->next = NULL;//初始化时,单链表不含任何元素,因此为空
return header;
}
/*
在单链表中添加指定的元素
*/
int insert_element(struct node* linkList, int x)
{
//为当前节点申请空间并赋值
struct node* item;
item = (struct node*)malloc(sizeof(struct node));
item->data = x;
//获取单链表中的首节点
struct node* firstNode = linkList->next;
//将当前节点作为链表的首节点
linkList->next = item;
//将之前的首节点连接到当前结点之后
item->next = firstNode;
return 1;
}
/*
删除某个位置的节点元素
*/
int delete_element(struct node* linkList, int position)
{
struct node *deleteNode, *preNode;//将要删除的节点,当前遍历的节点
if (position <= 0)
{
printf("位置错误\n");
return 0;
}
int j = 1;
deleteNode = linkList->next;//获得第一个元素
preNode = deleteNode;
//删除的是第一个节点
if (position == 1)
{
linkList->next = deleteNode->next;//如果删除的是第一个元素,则直接将第一个节点的下一节点链接到表头元素
free(deleteNode);//释放删除节点的内容
return 1;
}
while (j != position&&deleteNode != NULL)
{
preNode = deleteNode;//获得要删除的节点的前一节点,如果用双向链表的话,实现就会更加简单了
deleteNode = deleteNode->next;
j++;
}
/*
因为遍历到最后一个元素还找不到指定位置的元素,而尾节点的next是null,因此deleteNode==NULL表明到了表尾
*/
if (deleteNode == NULL)
{
printf("位置错误或表为NULL\n");
return 0;
}
else
{
preNode->next = deleteNode->next;//将删除的节点的后一节点连接到前一节点
free(deleteNode);
return 1;
}
}
/*
获得指定位置的节点数据
*/
int get_element(struct node *linkList, int position)
{
if (position <= 0)
{
printf("位置错误\n");
return 0;
}
if (linkList->next == NULL)
{
printf("表尾空\n");
return 0;
}
struct node *itemNode;
itemNode = linkList->next;
//表头元素
if (position == 1)
{
return itemNode->data;
}
int i = 1;
while (i != position&&itemNode != NULL)
{
itemNode = itemNode->next;
i++;
}
if (itemNode == NULL)
{
printf("位置错误\n");
return 0;
}
else
{
return itemNode->data;
}
}
运行结果:
4.1 init_linkList
初始化链表,这里其实就是初始化了一个表头元素,注意表头元素不是第一个节点元素。将表头元素的数据域设为-1,指针域设为null。
4.2 insert_element
为传递过来的值新建一个节点,然后获取当前链表的第一节点,赋值给新建的节点的指针域,再将新建的节点作为赋值给头节点的指针域。
4.3 delete_element
如果删除的是第一个节点元素,则直接把第一个节点元素的下一个节点赋值给表头的指针域即可。如果删除的是非第一个节点,则每次遍历的时候,都需保存当前(遍历到的节点)的节点,以及它的上一个节点。如果遍历到最后要删除的节点是NULL说明,指定要删除的位置大于链表的长度。如果不为NULL说明找到了要删除的节点。那么只需要将要删除的节点的指针域的元素赋值给上一节点的指针域,并释放删除的节点即可。
4.4 get_element
如果要获取的节点的链表的头节点是NULL说明链表是NULL,如果是第一个节点则直接获取头节点的下一个节点的data返回即可。如果非第一个节点那么就需要遍历。如果遍历之后当前的节点是NULL,说明指定的位置大于链表的长度。否则就返回当前节点的data。
5、C++实现单链表的基本运算
首先是Node.h头文件,里面定义了构造函数和链式表的基本操作:
#include <iostream>
class Node
{
public:
Node();
Node(int data, Node *next);
~Node();
/*
添加数据至头节点之后
*/
int insert_element(Node *header, int data);
/*
删除指定位置的节点元素
*/
int delete_element(Node *header, int position);
/*
获取指定位置的节点元素
*/
int get_element(Node header, int position);
/*
获取next元素
*/
Node* getNext()
{
return this->next;
}
/*
设置next元素
*/
void setNext(Node *item)
{
this->next = item;
}
/*
获取data元素
*/
int getData()
{
return this->data;
}
/*
设置data元素
*/
void setNext(int data)
{
this->data = data;
}
private:
int data;
Node* next;
};
Node::Node()
{
}
Node::Node(int data, Node *next)
{
this->data = data;
this->next = next;
}
Node::~Node()
{
}
/*
添加数据至头节点之后
*/
int insert_element(Node *header, int data)
{
//新建当前节点
//当我们使用关键字new在堆上动态创建一个对象时,它实际上做了三件事:获得一块内存空间、调用构造函数、返回正确的指针
Node *node = new Node(data, nullptr);
Node *pNode = node;
//获取单链表中的首节点的指针
Node* firstNode = header->getNext();
//将当前节点地址作为链表的首节点
header->setNext(pNode);
//将之前的首届点连接到当前结点之后
node->setNext(firstNode);
return 0;
}
/*
删除指定位置的节点元素
*/
int delete_element(Node *header, int position)
{
Node *deleteNode, *preNode;//将要删除的节点,当前遍历的节点
if (position <= 0)
{
std::cout << "位置错误" << std::endl;
return 0;
}
int j = 1;
deleteNode = header->getNext();//获得第一个元素
preNode = deleteNode;
//删除的是第一个节点
if (position == 1)
{
header->setNext(deleteNode->getNext());//如果删除的是首元素,则直接将第一个节点的下一节点链接到表头元素
free(deleteNode);//释放删除节点的内容
return 1;
}
while (j != position&&deleteNode != nullptr)
{
preNode = deleteNode;//获得要删除的节点的前一节点
deleteNode = deleteNode->getNext();
j++;
}
/*
因为遍历到最后一个元素还找不到指定位置的元素,而尾节点的next是null,因此deleteNode==NULL表明到了表尾
*/
if (deleteNode == nullptr)
{
std::cout << "位置错误或表为NULL" << std::endl;
return 0;
}
else
{
preNode->setNext(deleteNode->getNext());//将删除的节点的后一节点连接到前一节点
free(deleteNode);
return 1;
}
}
/*
获取指定位置的节点元素
*/
int get_element(Node header, int position)
{
if (position <= 0)
{
std::cout << "位置错误" << std::endl;
return 0;
}
if (header.getNext() == nullptr)
{
std::cout << "表为空" << std::endl;
return 0;
}
Node *itemNode;
itemNode = header.getNext();
//表头元素
if (position == 1)
{
return itemNode->getData();
}
int i = 1;
while (i != position&&itemNode != nullptr)
{
itemNode = itemNode->getNext();
i++;
}
if (itemNode == nullptr)
{
std::cout << "位置错误" << std::endl;
return 0;
}
else
{
return itemNode->getData();
}
}
然后是主函数文件用于测试:
#include <iostream>
#include "Node.h"
using std::cin;
using std::cout;
using std::endl;
int main()
{
Node header(-1, nullptr);
for (int i = 1; i <= 50; i++)
{
insert_element(&header, i);
}
cout << "打印1-50:" << endl;
for (int i = 1; i <= 50; i++)
{
cout << get_element(header, i) << " ";
if (i % 10 == 0)
cout << endl;
}
cout << endl;
cout << "试图删除越界元素:";
delete_element(&header, 60);//删除越界元素
delete_element(&header, 1);//删除首节点
cout << "50被删除掉了" << endl;
for (int i = 1; i <= 49; i++)
{
cout << get_element(header, i) << " ";
if (i % 10 == 0)
cout << endl;
}
cout << endl;
system("pause");
return 0;
}
运算逻辑与C语言的实现是一致的。
运行结果:
6、java实现单链表的基本运算
首先定义一个Node实体类:
/**
* java实现链式表的基本运算
* @author Hy
*
*/
public class Node {
private int data;
private Node next;
public int getData() {
return data;
}
public void setData(int data) {
this.data = data;
}
public Node getNext() {
return next;
}
public void setNext(Node next) {
this.next = next;
}
public Node(int data, Node next) {
super();
this.data = data;
this.next = next;
}
}
其次是定义一个实现基本运算的类:
public class NodeUtils {
/**
* 添加元素
*
* @param header
* @param data
* @return
*/
public static int insert_element(Node header, int data) {
// 新建当前节点
// 当我们使用关键字new在堆上动态创建一个对象时,它实际上做了三件事:获得一块内存空间、调用构造函数、返回正确的指针
Node node = new Node(data, null);
// 获取单链表中的首节点的指针
Node firstNode = header.getNext();
// 将当前节点地址作为链表的首节点
header.setNext(node);
// 将之前的首届点连接到当前结点之后
node.setNext(firstNode);
return 0;
}
/**
* 删除指定位置的元素
*
* @param header
* @param position
* @return
*/
public static int delete_element(Node header, int position) {
Node deleteNode, preNode;// 将要删除的节点,当前遍历的节点
if (position <= 0) {
System.out.println("位置错误");
return 0;
}
int j = 1;
deleteNode = header.getNext();// 获得第一个元素
preNode = deleteNode;
// 删除的是第一个节点
if (position == 1) {
header.setNext(deleteNode.getNext());// 如果删除的是首元素,则直接将第一个节点的下一节点链接到表头元素
deleteNode = null;// 将被删除的节点置为NULL等待垃圾回收机制回收
return 1;
}
while (j != position && deleteNode != null) {
preNode = deleteNode;// 获得要删除的节点的前一节点
deleteNode = deleteNode.getNext();
j++;
}
/*
* 因为遍历到最后一个元素还找不到指定位置的元素,而尾节点的next是null,因此deleteNode==NULL表明到了表尾
*/
if (deleteNode == null) {
System.out.println("位置错误或表为NULL");
return 0;
} else {
preNode.setNext(deleteNode.getNext());// 将删除的节点的后一节点连接到前一节点
deleteNode = null;
return 1;
}
}
/**
* 获取指定位置的元素
*
* @param header
* @param posistion
* @return
*/
public static int get_element(Node header, int position) {
if (position <= 0) {
System.out.println("位置错误");
return 0;
}
if (header.getNext() == null) {
System.out.println("表为空");
return 0;
}
Node itemNode;
itemNode = header.getNext();
// 表头元素
if (position == 1) {
return itemNode.getData();
}
int i = 1;
while (i != position && itemNode != null) {
itemNode = itemNode.getNext();
i++;
}
if (itemNode == null) {
System.out.println("位置错误");
return 0;
} else {
return itemNode.getData();
}
}
}
public class LinkList {
public static void main(String args[])
{
Node header=new Node(-1, null);
for (int i = 1; i <= 50; i++)
{
NodeUtils.insert_element(header, i);
}
System.out.println("打印1-50:" );
for (int i = 1; i <= 50; i++)
{
System.out.print( NodeUtils.get_element(header, i) +" ");
if (i % 10 == 0)
System.out.println();
}
System.out.println();
System.out.println( "试图删除越界元素:");
NodeUtils.delete_element(header, 60);//删除越界元素
NodeUtils.delete_element(header, 1);//删除首节点
System.out.println( "50被删除掉了" );
for (int i = 1; i <= 49; i++)
{
System.out.print( NodeUtils.get_element(header, i) + " ");
if (i % 10 == 0)
System.out.println();
}
System.out.println();
}
}
运行结果:
读者应该好好体会一下在C,C++,java中实现,各语言的特色和侧重点是什么。
7、循环链表
循环链表是一个可以从表尾重新回到表头的单链表的一种特例。它与单链表的不同之处在于,循环链表的尾节点的指针域指向的是头节点的地址。因此实现基本运算方面与单链表没什么不同,就是把判断尾节点的指针域为NULL变为是否是头节点。
8、双向链表
由前面的运算实现可以知道,每个节点都有一个后续节点,但是没有前驱节点。这就是说如果我们要查找某个节点的后续节点,时间渐进复杂度为O(1),但是查找前驱节点则必须从头开始,时间渐进复杂度为O(n)。为了缩小查找前驱的时间,可以使用空间换时间的策略,即给每个节点都定义一个前驱指针域用于指向前驱节点,这样每个节点都有一个前驱指针域,一个数据域,一个后续指针域。如果要设计成双向循环链表的话。就将头节点的前驱指针域指向尾节点,尾节点的后续指针域指向头节点。关于具体的运算实现,对比单链表,多了处理前驱指针域的任务,其它逻辑都不变。感兴趣的同学自己去实现一遍。