数据结构笔记1:链表
链表主要需要掌握,头插法尾插法创建链表。插入删除操作。还有就是遍历了,
很简单。
最关键的一点就是,考虑好头节点的应用。
Node.h
#ifndef _NODE_H_
#define _NODE_H_
//结构体一般定义在.h文件中
#define ElemType int
typedef struct LNode //单链表节点
{
ElemType data;
struct LNode *next;
}LNode,*LinkList;
/*与上面等价
struct LNode{
ElemType data;
struct LNode *next;
};
typedef struct LNode LNode; //LNode就是 struct LNode的一个别名
typedef struct LNode* LinkList; //LinkList是struct LNode*的一个别名
LNode a;//声明了一个struct LNode型变量a,与写 struct LNode a; 等价;
LinkList p;//声明了一个struct LNode *型指针变量p,与写 struct LNode *p; 等价。
*/
typedef struct DNode //双链表节点
{
ElemType data;
struct DNode *prior,*next;
}DNode,*DLinklist;
#endif
LinkList.h
#ifndef _LINKLIST_H_
#define _LINKLIST_H_
#include"Node.h"
using namespace std;
//单链表
LinkList LinkList_HeadInsert_Init(LinkList &L);
LinkList LinkList_TailInsert_Init(LinkList &L);
void LinkList_All_Out(LinkList L);
LinkList LinkList_Search(LinkList L,int i);
void LinkList_Insert(LinkList L,int i);
void LinkList_Delete(LinkList L,int i);
int LinkList_GetLenth(LinkList L);
//双链表
DLinklist DLinklist_Init(DLinklist &D);
void DLinklist_All_Out(DLinklist D);
DLinklist DLinklist_Search(DLinklist D,int i);
void DLinklist_Insert(DLinklist D,int i,bool j);
//这次写的函数都是传入的头结点,但是事实上很多情况下需要传入的
//是中间某个节点返回的p指针,比如先用search函数得到对应的指针。
#endif
LinkList.cpp
#include <iostream>
#include "LinkList.h"
#include "Node.h"
using namespace std;
/****************************单链表*********************************/
//最核心的思想,头结点的处理如何进行
//头插法,但是得到的元素和输入的元素是倒序的
LinkList LinkList_HeadInsert_Init(LinkList &L)
//引用,改变函数内L的值函数返回值也发生改变,同时引用定义了L,无需重新定义可直接调用
//需要对传入的参数进行改变时就要用引用
{
LinkList p;
int x;
L = new(LNode); //头结点,不存放数据 ,
//分配的内存是用来存放节点的,然后让一个指针指向这段内存。
L->next = NULL;
cin >> x;
while(x != 9999)
{
p = new(LNode); //创建新节点
p->data = x;
p->next = L->next; //插在头结点和现在第一个节点之间变成第一个节点
L->next = p;
cin >> x;
}
return L ;
}
//尾插法
LinkList LinkList_TailInsert_Init(LinkList &L)
{
int x;
LinkList p,r; //尾插法需要多定义一个指针作为中间变量
L = new(LNode);//要先分配空间,再把L赋给r
r = L;
cin >> x;
while(x != 9999)
{
p = new(LNode); //创建新节点
p->data = x;
r->next = p;
r = p;
cin >> x;
}
r->next = NULL;//尾插法让最后一个节点指向空即可
return L;
}
//遍历所有元素并全部输出
void LinkList_All_Out(LinkList L)
{
LinkList p = L->next;
while(p != NULL)
{
cout<< p->data <<endl;
p = p->next;
}
}
//访问特定元素并输出
LinkList LinkList_Search(LinkList L,int i)
{
LinkList p = L->next;
int j = 1;
if(i == 0) return L;
if(i<1) return NULL;
while(p->next) //判断条件(p->next)为真等价于判断(p->next != NULL)为真
{
if(j == i) break; //遍历+下标
j++;
p = p->next;
}
return p;
}
void LinkList_Insert(LinkList L,int i) //插入节点
{
LinkList p,q;
int x;
p = LinkList_Search(L,i);
q = new(LNode);
cin >> x ;
q->data = x;
q->next = p->next;
p->next = q;
}
void LinkList_Delete(LinkList L,int i)
{
int j = 1;
LinkList p,q;
p = L;
while(p && j < i)
{
j++;
p = p->next;
}
q = p->next; //这里为什么要把p->next保存给q呢
//因为我们最后要把删去的节点对应的内存给释放,注意释放的是指针指向的内存而并非指针本身。
//而执行了 p->next = p->next->next 之后,我们就无法找到该节点这段内存了,用q保存p->next,
//本质上保存的是这段内存,因此最后直接释放q,就相当于释放掉这段内存了。初始化q时也无需给q分配内存。
p->next = p->next->next;
delete q; //delete()和free()的操作对象是指针,清除的是指针指向的内存。
}
int LinkList_GetLenth(LinkList L)
{
LinkList p = L->next;
int n = 0;
while(p)
{
n++;
p = p->next;
}
return n;
}
/**************************************************************/
/**************************双链表*******************************/
DLinklist DLinklist_Init(DLinklist &D)
{
DLinklist p,q;
int x;
D = new(DNode);//建空双向循环链表
D->next = D;
D->prior = D;
q = D;
cin>>x;
while(x != 9999)
{
p = new(DNode);
p->prior = q;
q->next = p;
p->next = D;
D->prior = p;
p->data = x;
q = p;
cin>>x;
}
return D;
}
void DLinklist_All_Out(DLinklist D)
{
DLinklist p = D->next;
while(p != D)
{
cout<< p->data <<endl;
p = p->next;
}
}
DLinklist DLinklist_Search(DLinklist D,int i)
{
DLinklist p = D->next;
int j = 1;
if(i == 0) return D;
if(i<1) return NULL;
while(p->next) //判断条件(p->next)为真等价于判断(p->next != NULL)为真
{
if(j == i) break; //遍历+下标
j++;
p = p->next;
}
return p;
}
void DLinklist_Insert(DLinklist D,int i,bool j) //插入节点,bool为0,
//插入在i节点之后,为1插在之前
{
DLinklist p,q;
int x;
p = DLinklist_Search(D,i);
q = new(DNode);
cin >> x ;
q->data = x;
if(j == 0) //后插
{
p->next->prior = q;//先链接新增节点和未被移动指针p指向的节点
q->next = p->next; //1和2指令可对调,3和4指令可对调,
//但12要在34之前执行
p->next = q;
q->prior = p;
}
else //前插
{
p->prior->next = q;
q->prior = p->prior;
p->prior = q;
q->next = p;
}
}
void DLinklist_Delete(DLinklist D,int i) //删除访问到的那个节点
{
int j = 1;
DLinklist p,q;
p = D;
while(p && j < i)
{
j++;
p = p->next;
}
q = p->next;
p->next = p->next->next;
delete q;
}
main.cpp
#include <iostream>
#include "LinkList.h"
using namespace std;
// int main()
// {
// LinkList L;
// int num;
// //L = LinkList_HeadInsert_Init(L); //引用时在定义里用到了&L,因此在调用函数时直接将L传参进去即可
// L = LinkList_TailInsert_Init(L);
// //LinkList_All_Out(L);
// //p = LinkList_Search(L,5);
// //cout<<p->data<<endl;
// LinkList_Insert(L,5);
// LinkList_All_Out(L);
// LinkList_Delete(L,4);
// LinkList_All_Out(L);
// num = LinkList_GetLenth(L);
// cout << num << endl;
// //cout << L->next->next->data<<endl;
// return 0;
// }
#define Next 0
#define Prior 1
int main()
{
DLinklist D;
D = DLinklist_Init(D);
// cout << D->next->data <<endl;
// cout << D->prior->data <<endl;
DLinklist_Insert(D,5,Prior);
DLinklist_All_Out(D);
return 0;
}
易错整理:
一、链表总的首元结点、头结点、头指针的区别
三者的基本概念:
1、首元结点:就是指链表中存储第一个数据元素a1的结点。
2、头结点:它是在首元结点之前设的一个节点,其指针域指向首元结点。头
结点的数据域可以不存储信息,也可以存储与数据元素类型的其他附加信息。
3、头指针:它是指向链表中的第一个结点的指针。若链表设有头结点,则头
指针所指结点为线性表的头结点;若链表不设头结点,则头指针所指结点为该线
性表的首元结点。
4、尾指针:指向链表中的终端节点的指针。
链表增加头结点的作用有以下几点:
1、增加了头结点后,首元结点的地址保存在头结点(就是所说的“前驱”结点)的
指针域中,则对链表的第一个数据元素的操作与其他数据元素相同,无需进行特
殊处理
2、便于空表的和非空表的统一处理;当链表不设头结点时,假设L为单链表的头
指针,它应该指向首元结点,则当单链表为长度n为0的空表时,L指针为空(判断
空表的条件可记为:L==NULL)
3、增加头结点后,无论链表是否为空,头指针都是指向头结点的非空指针,若链
表为空的话,那么头结点的指针域为空。
二、单循环链表的尾指针
在单循环链表中,一般使用尾指针而非头指针。
尾指针是指向终端结点的指针,用它来表示单循环链表可以使得查找链表的开始
结点和终端结点都很方便。
设一带头结点的单循环链表,其尾指针为rear,则开始结点和终端结点的位置分别
是rear->next->next和rear,查找时间都是O(1)。 若用头指针来表示该链表,则查找
终端结点的时间为O(n)。
三、关于删除链表最后一个元素
因为删除链表最后一个元素需要访问到终端结点的前驱结点,让该节点指向
NULL,再将终端结点free掉,因此应该采用双链表便于访问前驱结点。
其实删除首元素也是同理的应灵活分析,关键在于,进行删除或者插入操作,需
要先访问哪个节点,用哪种链表最容易访问到。
四、关于引用传参和指针传参
先看一下插入结点的这个操作:
void LinkList_Insert(LinkList L,int i) //传入指针
或者
void LinkList_Insert(LinkList &L,int i) //引用操作
{
LinkList p,q;
int x;
p = LinkList_Search(L,i);
q = new(LNode); //给形参分配内存
cin >> x ;
q->data = x;
q->next = p->next;
p->next = q;
}
这种情况下,这两种操作其实都可以,没有本质区别。但是指针传参和引用有时
还是不同的,下面做一下辨析。
1、传递引用给函数与传递指针的效果是一样的。这时,被调函数的形参就成为
原来主调函数中的实参变量或对象的一个别名来使用,所以在被调函数中对形参
变量的操作就是对其相应的目标对象(在主调函数中)的操作。
2、使用引用传递函数的参数,在内存中并没有产生实参的副本,它是直接
对实参操作;而使用一般变量传递函数的参数,当发生函数调用时,需要给形参
分配存储单元,形参变量是实参变量的副本;如果传递的是对象,还将调用拷贝
构造函数。因此,当参数传递的数据较大时,用引用比用一般变量传递参数的效
率和所占空间都好。
3、使用指针作为函数的参数虽然也能达到与使用引用的效果,但是,在被
调函数中同样要给形参分配存储单元,且需要重复使用"*指针变量名"的形式进行
运算,这很容易产生错误且程序的阅读性较差;另一方面,在主调函数的调用点
处,必须用变量的地址作为实参。而引用更容易使用,更清晰。
4、如果既要利用引用提高程序的效率,又要保护传递给函数的数据不在函数
中被改变,就应使用常引用。
const int &d = c;
//常引用 是让 变量引用 具有只读属性 不能通过d 去修改c,但改变c会让d的值变