概念:
正如这幅图中所表示的那样,单链表就是由可能不连续的数据所组合而成的数据结构。 其中每个数据分为两部分,一部分是数据存储的位置,称为数据域,另外指针所存储的地方,称为指针域。
单链表节点定义
struct
struct node
{
int data; //存储链表数据
node *next; //存储结点的地址
node(int e=0,node *nextval=nullpte):data(e),next(nextval){}//构造函数
};
class
class node
{
int data;
node *next;
public:
node(int dval=0,node *nval=nullptr){
data=dval;
next=nval;
}
int getdata(){
return data;
}
node* getnext(){
return next;
}
void setdata(int dval){
data=dval;
}
void setnext(node* nval){
next=nval;
}
};
单链表类定义
成员函数
为带头结点单链表的成员函数
class list
{
private:
node *head; //头结点
public:
list(); //构建一个单链表
~list(); //销毁一个单链表
void creatlist(int num[],int n);//创建一个单链表
void inset(int pos,int value); //在第pos个节点后插入新节点,数据为value,如插入失败输出“error”
void print(); //以head为表头依次输出每个节点数据值
void remove(int pos); //删除第pos个节点,若不成功输出error
int getlength(); //获取单链表长度
node* find(int i); //查找第i个节点
};
单链表创建(带头结点)
表尾插入需要对head分空和非空的判断。同理,若插入、删除第一个结点,需要修改头指针head, 其它位置不需要,造成编程的麻烦。
为方便实现,增加头结点,即:head = new cnode(0,nullptr); 该结点不存数据。第一个数据在head->next。
头结点优点:head值始终不变。不需在程序中加入if…else。
头插
list::list(){
head=new node; //为头结点开辟内存空间
}
void list::creatlist(int num[],int n){
for(int i=0;i<n;i++){
node *s=new node(num[i],head->next); //将头指针所指向的下一个结点的地址,赋给新创建结点的next
head->next=s; //将新创建的结点的地址赋给头指针的下一个结点
}
}
输入:5
1 2 3 4 5
输出:5 4 3 2 1
头插法创建链表的根本在于深刻理解最后两条语句
s->next = head->next; // 将头指针所指向的下一个结点的地址,赋给新创建结点的next
head->next = s; // 将新创建的结点的地址赋给头指针的下一个结点
尾插
list::list(){
head=new node; //为头指针开辟内存空间
}
void list::creatlist(int num[],int n){
node *tail=head; //定义尾结点,未创建其余结点之前,只有一个头结点
for(int i=0;i<n;i++){
node *s=new node; //为新结点开辟新内存
tail->next=s; //将新开辟的node的地址赋给head的下一个结点地址。
s->data=num[i],s->next=nullptr; //新结点的数据域赋值
tail=s; //尾地址变为新节点
}
}
输入:5
1 2 3 4 5
输出:1 2 3 4 5
尾插法深刻理解:
①tail->next = s;
②tail = s;
①:第一个结点,tail 和 head 共用一块内存空间。现在从堆中心开辟出一块内存给 s,将 s 的数据域赋值后,此时 tail 中存储的地址是 head 的地址。此时,tail->next 代表的是头结点的指针域,因此 tail->next = s 代表的就是将上一个,也就是新开辟的 s 的地址赋给 head 的下一个结点地址。
②此时,tail->next 的地址是新创建的 s 的地址,而此时 tail 的地址还是 head 的地址。 因此 tail = s ,这条作用就是将新建的结点 s 的地址赋给尾结点 tail。 此时 tail 的地址不再是头结点,而是新建的结点 s。
总结
由上面的例子以及比较,我们可以看见:
- 头插法相对简便,但插入的数据与插入的顺序相反;
- 尾插法操作相对复杂,但插入的数据与插入顺序相同。
单链表创建(不带头结点)
(结点为类定义)
public:
list() :head(nullptr) {} //直接置为空
void createlistinhead(int num[], int n)
{
for (int i = 0; i < n; i++)
{
// new s
node* s = new node(num[i], head);
head = s;
}
}
void createlistintail(int num[], int n)
{
node* tail = nullptr;
for (int i = 0; i < n; i++)
{
// new s
node* s = new node(num[i]);
if (head == nullptr) //不带头结点加if else判断
head = s;
else
tail->setnext(s);
tail = s;
}
}
链表输出
带头结点
void list::print(){
node *p=head->next; //注意从head的下个结点开始,head不存数据
while(p!=nullptr){
cout<<p->data;
p=p->next;
}
cout<<endl;
}
不带头结点
void list::print(){
node *p=head; //注意从head开始,head存数据
while(p!=nullptr){
cout<<p->data;
p=p->next;
}
cout<<endl;
}
插入结点
void list::inset(int pos,int value){
if(pos<1||pos>getlength()) cout<<"error\n";
else{
node *temp=new node;
temp->data=value;
node *p=head->next; //如果不带头结点 node *p=head;
int i=1;
while(pos>i){
p=p->next;
i++;
}
temp->next=p->next;
p->next=temp;
}
}
删除结点
void list::remove(int pos){
if(pos<1||pos>getlength()) cout<<"error\n";
else{
if(pos==1){ //对头结点进行特判
node *temp=head->next; //创建占位结点指向头结点的下一个
delete head; //删除头结点
head=temp;
}else{
node *a=head,*b=head->next;
int i=1;
while(i<pos){ //a 循环到目标节点的前一个结点
a=b; //b 循环到目标节点
b=b->next;
i++;
}
a->next=b->next; //删除目标节点
delete b;
}
}
}
获取单链表长度
int list::getlength(){
int cnt=0;
node *p=head->next; //注意
while(p!=nullptr){
cnt++;
p=p->next;
}
return cnt;
}
查找结点
node*list::find(int i){
if(i<0||i>getlength())
return nullptr;
node *p=head;
for(int k=0;k<i&&p;k++)
p=p->next;
return p;
}