单链表
单链表
- 每个结点除了存档元素外,还要存储下一个结点的指针
- 优点:不要求大片连续空间,改变容量方便
- 缺点:不可随机存取
用代码定义一个单链表
struct LNode{ //定义单链表结点类型
ElemType data; //每个结点存放一个数据元素(数据域)
struct LNode *next; //指针指向下一个结点
};
增加一个新结点:在内存中申请一个结点需要空间,并用指针p指向这个结点
struct LNode *p = (struct LNode *)malloc(sizeof(struct LNode));
-
typedef关键字——数据类型重命名
typedef struct LNode LNode;//先对struct LNode重命名为LNode,简便后续操作 LNode *p = (LNode *)malloc(sizeof(LNode));
// 书本上更简洁的命名方法
typedef struct LNode{//定义单链表结点类型
ElemType data;
struct LNode *next;
}Node,*LinkList;//这两个都是struct LNode的重命名
上面的代码等价于
struct LNode{
ElemType data;
struct LNode *next;
};
typedef struct LNode LNode;
typedef struct LNpde *LinkList;
这样,如果要表示一个单链表时,只需要声明一个头指针L,指向单链表的第一个结点
LNode* L;
LinkList L;//或者这样都可以声明一个指向单链表第一个结点的头指针,这种当时代码可读性更强
-
具体有什么区别,看下面的代码
typedef struct LNode{ ElemType data; struct LNode *next; }LNode, *LinkList; LNode * GetElem(LinkList L, int i){//这里两种声明方式都用到了,其实LinkList L也完全可以用LNode *L来替换,但是为什么要这么写呢?原因是:——当我们使用LinkList时,我们强调这是一个单链表;——当我们使用LNode *时,我们强调这是一个结点 int j = 1; LNode * p = L->next; if(i == 0) return L; if(i < 1) return NULL; while(p != NULL && j < i){ p = p->next; j++; } return 0; }
初始化单链表
- 不带头结点的单链表
typedef struct LNode{//定义单链表结点类型
ElemType data;//每个结点存放一个数据元素
struct LNode *next;//指针指向下一个结点
}LNode, *LinkList;
//初始化一个空的单链表
bool InitList(LinkList &L){//如果这里不用&引用符号,那么是改变不了主函数中的L的,即不能把主函数中的L初始化,改变的是主函数L的另为一个副本
L = NULL;//空表,暂时还没有任何结点(防止脏数据)
return true;
}
//判断单链表是否为空
bool Empty(LinkList L){
if(L == NULL)
return true;
else
return false;
}
//或者,因为L==NULL判断之后返回的就是true或者false所以,下面的写法更简洁
bool Empty(LinkList L){
return(L==NULL);
}
void test(){
LinkList L;//声明一个指向单链表的指针
//初始化一个空表
InitList(L);
//....后续代码...
}
- 带头结点的单链表
typedef struct LNode{ ElemType data; struct LNode *next;}LNode, *LinkList;//初始化一个单链表(带头结点)bool InitList(LinkList &L){ L = (LNode *)malloc(sizeof(LNode));//分配一个头结点 if(L==NULL)//内存不足,分配失败 return false; L->next = NULL;//头结点之后暂时还没有结点 return true;}//判断单链表是否为空(带头结点)bool Empty(LinkList L){ if(L->next == NULL) return true; else return false;}void test(){ LinkList L;//声明一个指向单链表的指针 InitList(L);//初始化一个空表 //....后续代码...}
带头结点的指针写代码会更加方便
单链表的插入删除
插入
-
按位序插入:带头结点的插入ListInsert(&L, i,e):在表L中的第i个位置上插入指定元素e
typedef struct LNode{ ElemType data; struct LNode *next;}LNode, *LinkList;//在第i个位置插入元素e(带头结点)bool ListInsert(LinkList &L, int i, ElemType e){ if(i<1) return false; LNode *p;//指针p指向当前扫描到的结点 int j = 0;//当前p指向的是第几个结点 p = L;//L指向头结点,头结点是第0个结点(不存数据) while(p!=NULL && j<i-1){//循环找到第i-1个结点 p = p->next; j++; } if(p==NULL)//i值不合法 return false; LNode *s =(LNode *)malloc(sizeof(LNode)); s->data = e; s->next = p->next; p->next = s;//将结点s连到p之后 return true;//插入成功}
-
按位序插入:不带头结点的插入:找到第i-1个结点,把新结点插在其后面。不存在“第0个”结点,因此当i=1的时候需要特殊处理
typedef struct LNode{ ElemType data; struct LNode *next;}LNode, *LinkList;//在第i个位置插入元素e(带头结点)bool ListInsert(LinkList &L, int i, ElemType e){ if(i<1) return false; if(i == 1){//插入第1个结点的操作与其他结点操作不同 LNode *s = (LNode *)malloc(sizeof(LNode)); s->data = e; s->next = L; L = s;//头指针指向新节点 } LNode *p;//指针p指向当前扫描到的结点 int j = 1;//当前p指向的是第几个结点 p = L;//L指向头结点,头结点是第0个结点(不存数据) while(p!=NULL && j<i-1){//循环找到第i-1个结点 p = p->next; j++; } if(p==NULL)//i值不合法 return false; LNode *s =(LNode *)malloc(sizeof(LNode)); s->data = e; s->next = p->next; p->next = s;//将结点s连到p之后 return true;//插入成功}
-
后插操作:在p结点之后插入元素e
bool InsertNextNode (LNode *p, ElemType e){ if(p == NULL) return false; LNode *s = (LNode *)malloc(sizeof(LNode)); //判断内存分配是否成功(某些情况下有可能分配失败,如内存不足,但是考试手写时也可以不写) if(s==NULL) return false; s->data = e; s->next = p->next; p->next = s; return true;}
-
前插操作:在p结点之前插入元素e
bool InsertPriorNode(LNode *p, LNode *s){ if(p==NULL || s = NULL) return false; s->next = p->next; p->next = s; ElemType temp = p->data; p->data = s->data; s->data = temp; return false;}
删除
- ListDelete(&L,i,&e):删除操作。删除表L中第i个位置的元素,并用e返回删除元素的值
typedef struct LNode{ ElemType data; struct LNode *next;}LNode, *LinkList;//在第i个位置插入元素e(带头结点)bool ListInsert(LinkList &L, int i, ElemType e){ if(i<1) return false; LNode *p;//指针p指向当前扫描到的结点 int j = 0;//当前p指向的是第几个结点 p = L;//L指向头结点,头结点是第0个结点(不存数据) while(p!=NULL && j<i-1){//循环找到第i-1个结点 p = p->next; j++; } if(p==NULL)//i值不合法 return false; LNode *q = p->next; e = p->data; p->next = q->next; free(q); return true;
-
删除指定结点
如果p结点是最后一个结点的话,p->next->data = NULL,就会出现空指针的错误,如果出现这种情况,那我们就只能引入头指针,来从头开始一个个搞,
bool DeleteNode(LNode *p){ if(p==NULL) return false; LNode *q = p->next; p->data = p->next->data; p->next = q->next; free(q); return true; }
单链表的查找
-
按位查找:GetElem(L,i)。获取表L中第i个位置的元素的值
//带头结点,返回第i个元素LNode * GetElem(LinkList L,int i){ if(i<0) return NULL; LNode *p;//指针p指向当前扫描到的结点 int j = 0;//当前p指向的是第几个结点 p = L;//L指向头结点,头结点是第0个结点(不存数据) while(p!=NULL && j<i){//循环找到第i个结点 p = p->next; j++; } return p;}
-
按值查找:LocateElem(L,e)。在表L中查找具有给定个关键字值的元素
//按值查找,找到数据域==e的结点LNode *LocateElem(LinkList L,ElemType e){ LNode *p = L->next;//从第一个结点开始查找数据域为e的结点 while(p!=NULL && p->data !=e) p = p->next; return p;//找到后返回该结点指针,否则返回NULL}
求表的长度
int Length(LinkList L){
int len = 0;
LNode *p = L;
while(p->next != NULL){
p = p->next;
len++;
}
return len;
}
单链表的建立
step1.初始化一个单链表
step2.每次取一个数据元素,插入到表尾/表头
尾插法建立单链表
//定义单链表
typedef struct LNode{
ElemType data;
struct LNode *next;
}LNode, *LinkList;
//对指定结点的后插操作
LinkList List_Taillnsert(LinkList &L){
int x;
L = (LinkList)malloc(sizeof(LNode));//初始化空表,建立头结点
LNode *s, *r = L;//r为指向表尾的指针
scanf("%d", &x);//输入结点的值
while(x!=9999){//输出入9999表示结束
s = (LNode *)malloc(sizeof(LNode));
s->data = x;
r-next = s;
r = s;//永远保持r指向最后一个结点
}
r->next = NULL;//尾结点指针置空
return L;
}
头插法建立单链表
- 头插法的重要应用,链表的逆置。原理是头插法读入数据的顺序与生成链表中元素的顺序是相反的。
- 所以我们可以在指针扫描各个结点的时候,取出各个数据,在用头插法插入到头结点的后面,这样就可以实现链表逆置了
LinkList List_HeadInsert(LinkList &L){
LNode *s;
int x;
L = (LinkList)malloc(sizeof(LNode));
L->next = NULL;
scanf("%d",&x);
while(x!=9999){
s = (LNode *)malloc(sizeof(LNode));
s->data = x;
s->next = L->next;
L->next = s;
scanf("%d", &x);
}
return L;
}