链表及其实现
链式结构
顺序表插入、删除时间代价的分析,可以看出其时间复杂度是线性阶的,而且会引起大量已存储元素的位置移动。
改进方法:链式结构
链表的存储映像图
为了清晰看出逻辑关系,以后链表用图(b)来表示。
结构类型、结构变量、结构指针
#结构体
struct dateT
{ int year;
int month;
int day;
};
#类
class dateT
{ public:
int year;
int month;
int day;
};
#结构变量、结构指针
dateT d, *p;
d.year = 2020;
d.month = 3;
d.day = 11;
p = &d;
cout<<p->year<<endl;
cout<<p->month<<endl;
cout<<p->day<<endl;
dateT d, *p;
p = new dataT;
p->year = 1010;
p->month = 1;
d->day = 1;
delete p;
p->year = 2021; //非法
p = &d; //合法
*p在此是指针,在后面delete p,系统回收的是无名氏对应的内存,而不是指针,故可以在后面重新将指针指向没被回收的d内存,p=&d表示指针p指向内存d的起始地址。指针只会在程序结束时由系统自动回收。
结点和链表
单链表的特点
单链表类
单链表结点类(linkList.h)
class outOfBound{};
template <class elemType>
class linkList; //类的前向说明
template <class elemType>
class node
{ friend class linkList<elemType>;
private:
elemType data;
node *next;
public:
node():next(NULL){};
node(const elemType &e, node *N=NULL)
{ data = e; next = N; };
};
单链表类
template <class elemType>
class linkList
{ private:
node<elemType> *head;
public:
linkList(); //构造函数,建立一个空表
bool isEmpty ()const; //表为空返回true,否则返回false。
bool isFull ()const {return false;}; //表为满返回true,否则返回false。
int length ()const; //表的长度
elemType get(int i)const;//返回第i个元素的值
//返回值等于e的元素的序号,从第1个开始,无则返回0.
int find (const elemType &e )const;
//在第i个位置上插入新的元素(值为e)。
void insert (int i, const elemType &e );
//若第i个元素存在,删除并将其值放入e指向的空间。
void remove (int i, elemType &e);
void reverse()const; //元素就地逆置
void clear (); //清空表,使其为空表
~linkList();
};
链表基本操作的实现代码(linkList.h)
template <class elemType> //属性赋初值,模板函数用法
linkList<elemType>::linkList() //构造函数,建立一个空表
{
head = new node<elemType>();
}
template <class elemType>
bool linkList<elemType>::isEmpty ()const //表为空返回true,否则返回false。
{
if (head->next==NULL) return true;
return false;
}
链表基本操作的实现分析
插入总结:遵循“先武装自己,再融入队伍”
1.在内存中创建新结点。
2.武装新结点:将x写入新结点的data字段,p指针所指结点的下一结点地址写入新结点的 next字段,使p所指结点的下一结点成为新结点的直接后继结点。
3.将新结点地址写入p的next字段,使新结点成为p所指结点的直接后继结点。
具体语句
#法一
tmp = new node<elemType>();
tmp->data = e;
tmp->next = p->next;
p->next = tmp;
#法二
tmp = new node<elemType>(e, p->next);
p->next = tmp;
#法三:四合一语句
p->next = new node<elemType>(e, p->next);
时间复杂度分析:
当P已经指向了插入位置的前一个结点时,插入操作和结点个数无关,时间复杂度为O(1)。
链表基本操作实现代码
template <class elemType>
void linkList<elemType>::insert (int i, const elemType &e )
//在第i个位置上插入新的元素(值为e)。
{ if (i<1) return;//参数i越界
int j=0; node<elemType> *p=head;
while (p&&j<i-1) //注意此处一定要添加p这一条件,以便确保指针合法
{ j++; p=p->next; }
if (!p) return; //参数i越界
p->next = new node<elemType>(e, p->next);
}
删除操作: 删除P指针所指结点之后的那个结点
删除总结:
具体语句:
node *q=p->next;
p->next = q->next;
delete q;
时间复杂度分析:
当P已经指向了待删除结点的前一个结点时,删除操作和结点个数无关,时间复杂度为O(1)。
查找操作:
- 找值为x的结点,顺首结点逐个向后检查、匹配。
- 单链表和顺序表中,时间复杂度都是O(n)。
- 找第k个结点,顺序表O(1),链表O(n)。
其他基本操作:
isFull:因每次只申请一个结点空间,故总为false。
clear:除了头结点删除并释放整个单链表中结点,回到初始化后的状态。
template <class elemType>
int linkList<elemType>::length ()const //表的长度
{
int count=0;
node<elemType> *p;
p=head->next;
while(p)
{ count++; p=p->next; }
return count;
}
template <class elemType> //注意:五步口诀法
elemType linkList<elemType>::get(int i )const
//返回第i个元素的值,首元素为第1个元素
{
if (i<1) throw outOfBound();
int j=1;
node<elemType> *p = head->next;
while (p&&j<i) {p=p->next; j++;}
if (p) return p->data;
throw outOfBound();
}
template <class elemType>
int linkList<elemType>::find (const elemType &e )const
//返回值等于e的元素的序号,从第1个开始,无则返回0.
{ int i=1;
node<elemType> *p = head->next;
while (p)
{ if (p->data==e) break;
i++; p=p->next;
}
if (p) return i;
return 0; }
两种常用技巧:兄弟协同法、首席插入法
兄弟协同法(使用两个指针,在此是指p指针和q指针)
template <class elemType> //P、Q兄弟协同法
void linkList<elemType>::clear () //清空表,使其为空表
{ node<elemType> *p,*q;
p=head->next; head->next=NULL;
while (p)
{ q=p->next;
delete p;
p=q;
}
}
最快插入位置——“脖子”(首席插入法)
template <class elemType>
void linkList<elemType>:: insert(const elemType a[], int n)
{ node *tmp,
for (int i=0; i<n; i++)
{ tmp = new node(a[i], head->next); head->next = tmp; }
}
a[5]={1,3,5,7,9}
练习
练习描述:对一个单链表进行就地逆置---摆龙门阵
“兄弟协同法”+“首席插入法”实现单链表的就地逆置
template <class elemType>
void linkList<elemType>::reverse()
{ node<elemType> *p,*q; //兄弟俩协同
p=head->next; head->next = NULL;
while (p)
{ q = p->next;
p->next = head->next; head->next = p; //首席插入
p=q;
}
}
常见错误
Ø指针p未被初始化或者为空,读取其指向的字段如p->data,如循环检查p所指的结点中值是否x,可用while (p && p->data!=x) p=p->next。
Øp原本指向了一个结点,但其指向的结点空间已经释放,仍要读取其所指结点的字段。如p=head; delete p; p=p->next;
p->next非法访问了不能访问的内存空间。