链表(Linked list)是一种常见的基础数据结构,是一种线性表,但是并不会按线性的顺序存储数据,而是在每一个节点里存到下一个节点的指针(Pointer),简单来说链表并不像数组那样将数组存储在一个连续的内存地址空间里,它们可以不是连续的因为他们每个节点保存着下一个节点的引用(地址),所以较之数组来说这是一个优势。
单链表的特点
链表增删元素的时间复杂度为O(1),查找一个元素的时间复杂度为 O(n);
单链表不用像数组那样预先分配存储空间的大小,避免了空间浪费;
单链表不能进行回溯操作,如:只知道链表的头节点的时候无法快读快速链表的倒数第几个节点的值。
1、单链表的定义
2、单链表的初始化
3、头插法建立单链表
4、尾插法建立单链表
5、打印单链表
6、获取单链表的长度
7、查询指定索引的节点值或指定节点值的索引
8、删除指定位置的结点
9、在指定位置插入结点
10、单链表的逆置
typedef int elemtype; //取别名,element type(“元素的类型”)
typedef struct node
{
elemtype data; //数据域
struct node *next; //指针域
}
node, *linklist;
相当于有两步:typedef struct node node; typedef struct node *linklist;
先给struct node 创建一个简单的别名,以后就用node代替它了。然后通过typedef创建了一个linklist指针代表所有指向这个结构的指针,而不是说linklist是指向node类型的指针。
linklist LinkedListInit()
{
linklist l; //node *l;
l = (node *)malloc(sizeof(node)); //申请结点空间
if(l == NULL) //判断是否有足够的内存空间
printf("申请内存空间失败\n");
l->next = NULL; //将next设置为NULL,初始长度为0的单链表
}
C++提供了指向结构体变量的运算符->
,l->next = NULL
和 (*l).next=NULL
等价。
直接在全局定义的结构体,存储在静态存储区;在函数内定义的结构体,存储在栈区;而使用malloc来申请空间的结构体,存储在堆空间中。链表一般都放在堆空间中。
malloc用法:
函数原型:void *malloc(unsigned int num_bytes);
//分配长度为num_bytes字节的内存块
返回值是void指针,void* 表示未确定类型的指针,void *可以指向任何类型的数据,更明确的说是指申请内存空间时还不知道用户是用这段空间来存储什么类型的数据(比如是char还是int或者其他数据类型),可以通过类型强制转化转化为其他任意类型指针。如果分配成功则返回指向被分配内存的指针(此存储区中的初始值不确定),否则返回空指针NULL。
L = (node *)malloc(sizeof(node));
就在内存中给l分配了一个动态的存储空间。(node *)为强制转换,把返回类型void *转换为node *,sizeof(node)为获取node类型占据空间的大小,如在我机子上int类型占4字节,sizeof(int)就返回4;
头插入法创建单链表的思路:在上一个新加入的数据域前面加入这次新加入的数据域。首先创建一个空表,生成一个新的节点;并将读取到的数据放入新节点的数据域中,然后将该节点插入到当前链表的表头,即就是头结点之后;直到插入元素完成。
(新节点先拷贝头的指针域,再令头的指针域指向新节点)
采用头插入法创建的单链表的逻辑顺序与输入的数据顺序是相反的,所以头插入法是一个逆序建表法。
写法一:(严谨写法)
node* creatfromhead()/*头插法建立单链表*/
{
linklist l;
node *s;
int c;
int flag = 1;
l = (node*)malloc(sizeof(node));
if (l == NULL)
{
printf("申请空间失败!!");
return 0;
}
l->next = NULL; //初始化
while (flag)
{
scanf("%d", &c);
if (c != 0) //输入单链表的时候以“0”结束输入
{
s = (node*)malloc(sizeof(node));
if (s == NULL)
{
printf("申请空间失败!!");
return 0;
}
s->data = c;
s->next = l->next;
l->next = s;
}
else flag = 0;
}
return l;
}
写法二:(简洁写法)
linklist LinkedListCreatH()
{
node *L;
L = (node *)malloc(sizeof(node)); //申请头结点空间
L->next = NULL; //初始化一个空链表
elemtype x; //x为链表数据域中的数据
while (scanf("%d", &x) != EOF)
{
node *p;
p = (node *)malloc(sizeof(node)); //申请新的结点
p->data = x; //结点数据域赋值
p->next = L->next; //将结点插入到表头L-->|2|-->|1|-->NULL
L->next = p;
}
return L;
}
注意: while (scanf("%d", &x) != EOF)输入如何结束?
VS2015中:输完数据 -> 回车 -> ctrl+Z -> 回车 -> ctrl+Z -> 回车 -> ctrl+Z -> 回车
VC++6.0中:输完数据 -> 回车 -> ctrl+Z -> 回车
尾插法即将新数据值插入到链表的尾部,需要新建一个辅助指针r来指向终端结点。采用尾插入法创建的单链表的逻辑顺序与输入的数据顺序是相同的。
linklist LinkedListCreatT()
{
node *L;
L = (node *)malloc(sizeof(node)); //申请头结点空间
L->next = NULL;
node *r;
r = L; //r始终指向终端结点,开始时指向头结点。此时的头结点是终端结点
elemtype x; //x为链表数据域中的数据
while (scanf("%d", &x) != EOF)
{
node *p; //p用来指向新生成的节点
p = (node *)malloc(sizeof(node)); //申请新的结点
p->data = x; //用新节点的数据域来接受x
r->next = p; //用r来接纳新节点,将结点插入到表头L-->|1|-->|2|-->NULL
r = p; //r指向终端节点
}
r->next = NULL; //元素已经全部装入链表L中 L的终端节点指针域为NULL,L建立完成
return L;
}
node* get(linklist l)/*单链表的遍历*/
{
node *p;
p = l->next;
while (p != NULL)
{
printf("%d ", p->data);
p = p->next;
}
printf("\n");
return l;
}
由于单链表的存储地址不是连续的,链表并不具有直接获取链表长度的功能,对于一个链表的长度我们只能一次去遍历链表的节点,直到找到某个节点的下一个节点为空的时候得到链表的总长度,注意这里的出发点并不是一个空链表然后依次添加节点后,然后去读取已经记录的节点个数,而是已知一个链表的头结点然后去获取这个链表的长度:
int getLength(node *head){
int len = 0;
node *p;
p=head; //新不新建指针都可以,不会影响主调函数的实参变量的值
while(p!= NULL){
len++;
p = p->next;
}
return (len);
}
由于链表是一种非连续性的存储结构,节点的内存地址不是连续的,也就是说链表不能像数组那样可以通过索引值获取索引位置的元素。所以链表的查询的时间复杂度要是O(n)级别的,这点和数组查询指定值得元素位置是相同的,因为你要查找的东西在内存中的存储地址都是不一定的。
/** 获取指定角标的节点值 */
int getValueOfIndex(node *head, int index){
if (index < 0 || index >= getLength(head)) {
throw "角标越界!";
}
if (head == NULL) {
throw "当前链表为空!";
}
node *dummyHead = head;
while (dummyHead->next != NULL && index > 0) {
dummyHead = dummyHead->next;
index--;
}
return dummyHead->data;
}
/** 获取节点值等于 value 的第一个元素角标 */
int getNodeIndex(node *head, int data) {
int index = -1;
node *dummyHead = head;
while (dummyHead != NULL) {
index++;
if (dummyHead->data == data) {
return index;
}
dummyHead = dummyHead->next;
}
return -1;
}
单链表的插入,在链表的第i个位置插入x的元素
linklist LinkedListInsert(linklist L,int i,elemtype x)
{
node *pre; //pre为前驱结点
pre = L;
int tempi = 0;
for (tempi = 1; tempi < i; tempi++)
pre = pre->next; //查找第i个位置的前驱结点
node *p; //插入的结点为p
p = (node *)malloc(sizeof(node));
p->data = x;
p->next = pre->next;
pre->next = p;
return L;
}
在链表中删除值为x的元素
linklist LinkedListDelete(linklist L,elemtype x)
{
node *p,*pre; //pre为前驱结点,p为查找的结点。
p = L->next;
while(p->data != x) //查找值为x的元素
{
pre = p;
p = p->next;
}
pre->next = p->next; //删除操作,将其前驱next指向其后继。
free(p);
return L;
}
顺序访问原列表元素,然后把这个元素插入到L的头部
node* reverselist(linklist l)/*将带头结点的单链表逆置*/
{
node *q, *p;
p = l->next;
l->next = NULL;
while (p != NULL)
{
q = p->next;
p->next = l->next;
l->next = p;
p = q;
}
return l;
}