单链表
链表是常用的数据结构,其优点是插入和删除元素时不需要移动,表的容量可扩充,且存储空间可以不连续。
另外,由于涉及到指针,所以很受面试官的青睐。
本文将主要介绍单链表,并简单介绍下双链表和环形链表,并通过一系列的题目来强化这方面的知识。
链表节点的结构:
template<class DataType>
struct Node
{
DataType data;
Node<DataType> *next;
};
data存放节点的数据,next指向下一个节点。
对于单链表,需要设置头指针,指向第一个元素所在的节点,所有的操作都是从头指针开始的。
有时候我们可以设置一个哨兵,它也是一个节点,称为头节点,该节点不存放数据,仅用于简化代码(加上头结点之后,无论链表是否为空,头指针始终指向头结点,因此空表和非空表的处理统一一点)。
下面我们将以有哨兵的链表为例来实现单链表。
template<class DataType>
class LinkList
{
public:
LinkList();
LinkList(DataType a[], int n);
~LinkList();
DataType get(int i); //按位查找,第i个节点
int locate(DataType x); //按值查找,返回x的位置序号
void insert(int i, DataType x); //在第i个位置插入x
DataType erase(int i);
void print();
private:
Node<DataType> *first; //头指针
};
构造函数
template<class DataType>
LinkList<DataType>::LinkList()
{
first = new Node<DataType>;
first->next = NULL;
}
//头插法
template<class DataType>
LinkList<DataType>::LinkList(DataType a[], int n)
{
first = new Node<DataType>;
first->next = NULL;
Node<DataType> *newNode;
for (int i = 0; i < n; ++i)
{
newNode = new Node<DataType>;
newNode->data = a[i];
newNode->next = first->next;
first->next = newNode;
}
}
//尾插法
template<class DataType>
LinkList<DataType>::LinkList(DataType a[], int n)
{
first = new Node<DataType>;
Node<DataType> *rail, *newNode;
rail = first;
for (int i = 0; i < n; ++i)
{
newNode = new Node<DataType>;
newNode->data = a[i];
rail->next = newNode;
rail = newNode;
}
rail->next = NULL;
}
析构函数
template<class DataType>
LinkList<DataType>::~LinkList()
{
Node<DataType> *cur;
while (first != NULL)
{
cur = first; //暂存释放节点
first = first->next;
delete cur;
}
}
遍历操作
template<class DataType>
void LinkList<DataType>::print()
{
Node<DataType> *cur = first->next;
while (cur)
{
cout << cur->data << ' ';
cur = cur->next;
}
cout << endl;
}
按位查找
template<class DataType>
DataType LinkList<DataType>::get(int i)
{
Node<DataType> *cur = first->next;
int pos = 1;
while (cur && pos != i)
{
cur = cur->next;
++pos;
}
if (NULL == cur)
throw "查找失败";
else
return cur->data;
}
按值查找
template<class DataType>
int LinkList<DataType>::locate(DataType x)
{
Node<DataType> *cur = first->next;
int pos = 1;
while (cur && cur->data != x)
{
cur = cur->next;
++pos;
}
if (NULL == cur)
return 0; //查找失败
else
return cur->data;
}
插入
template<class DataType>
void LinkList<DataType>::insert(int i, DataType x)
{
//考虑i=1的情况,我们需要从哨兵开始
Node<DataType> *cur = first;
int pos = 0;
//查找第i-1个位置
while (cur && pos != i-1)
{
cur = cur->next;
++pos;
}
if (NULL == cur)
throw "插入失败";
else
{
Node<DataType> *newNode = new Node<DataType>;
newNode->data = x;
newNode->next = cur->next;
cur->next = newNode;
}
}
删除
template<class DataType>
DataType LinkList<DataType>::erase(int i)
{
Node<DataType> *cur = first;
int pos = 0;
int ret;
//查找第i-1个位置
while (cur && pos != i-1)
{
cur = cur->next;
++pos;
}
if (NULL == cur || NULL == cur->next) //注意,第i-1个节点找到了,可能第i个节点不存在!
throw "插入失败";
else
{
Node<DataType> *tmpNode = cur->next; //暂存
ret = tmpNode->data;
cur->next = tmpNode->next; //摘链
delete tmpNode;
return ret;
}
}
循环单链表
循环链表只是在单链表的基础上使其首尾相连。
对于循环链表,如果还是用first指向头指针,由于我们只有next标志,没有pre标志,所以并不能很方便地找到尾部。
因此,在循环链表中,我们常常使用尾指针rear来指示最后一个节点。如此一来,使用rear->next->next即可取得第一个节点(rear->next为哨兵),rear则取得最后一个节点,这样子对首尾的访问就便利许多。
双链表
双链表比单链表的节点多了一个prior来指向前驱节点:
template<class DataType>
struct DulNode
{
DataType data;
DulNode<DataType> *prior, *next;
};
双链表的大多数操作跟单链表类似,它的优点是“能进能退”,可以方便地访问前驱后继。
插入
//在p节点后插入新节点s
s->prior = p; //插入
s->next = p->next; //插入
p->next->prior = s; //换链
p->next = s; //换链
删除
//p指向待删除节点
p->prior->next = p->next;
p->next->prior = p->prior;
delete p;
静态链表
静态链表是用数组来表示链表,用数组元素的下标来模拟单链表的指针。这种表示方法比较灵活,而且速度比较快,不过空间限制比较大。
一个比较典型的例子是: 移动小球
该例可以使用两个数组left[],right[]来模拟双链表,以提高效率。
常用的静态链表存储结构:
const int MaxSize = 100;
template <class DataType>
struct SNode
{
DataType data;
int next
} SList[Maxsize];
静态链表需要两个指针:first为静态链表的头指针;avai是空闲链的头指针。
也就是说,我们的SList将分为两条链,一条是已使用的,一条是空闲的。
为方便运算,我们的静态链表也带上头节点。
//初始化
first = 0;
SList[first].next = -1; //已使用链只有头节点
avail = 1; //剩下的节点串成空闲链
for (int i = avail; i < MaxSize-1; ++i)
{
SList[i].next = i+1;
}
SList[MaxSize-1].next = -1;
//在节点p后面插入新节点
if (-1 == avail)
throw "链表已满";
int newNodeIndex = avail; //获取一个空闲的节点
avail = SList[avail].next;
SList[newNodeIndex].next = SList[p].next;
SList[p].next = newNodeIndex;
//删除节点p的后继节点
int q = SList[p].next; //暂存被删除的节点
SList[p].next = SList[q].next; //摘链
SList[q].next = avail; //删除的节点插到空闲链头部
avail = q;
**插入删除只需要修改游标,不需要移动元素。
相关题目
在O(1)时间删除链表节点
给定单向链表的头指针和一个节点指针,定义一个函数在O(1)时间删除该节点。链表节点和函数定义如下:
struct ListNode
{
int value;
ListNode *next;
};
void DeleteNode(ListNode** head, ListNode* p);
首先时间的限定使得我们不能从头开始遍历。
可以肯定的是,要删除节点p,我们需要让p的前驱的next指向p的后继。我们常规的想法是改变p的前驱的next,但是由于不能直接访问到,所以山不过来,我过去——将p的后继移动到p的位置上。
于是问题就很简单了:
如果p的后继存在,记为q,那么我们将q复制到p上,之后就可以对q的原位置进行解链并释放内存了,间接地删除了节点p(实际上p处的内存并没有释放,释放的是p的后继的内存)。
需要注意的特殊情况是,如果p没有后继,那么就不能用以上方法来解决了,此时仍然需要从头开始遍历。
另外一个特殊情况是,如果链表中只有一个节点,那么删除之后,需要将head置为NULL。
void DeleteNode(ListNode** pHead, ListNode* p)
{
if (!pHead || !p)
return;
if (p->next) //存在后继节点
{
ListNode *pNext = p->next;
p->value = pNext->value;
p->next = pNext->next;
delete pNext;
}
else if (*head == p) //只有一个节点,头节点
{
*head = NULL;
delete p;
p = NULL;
}
else //多个节点,删除尾节点
{
ListNode *pNext = *head;
while (pNext->next != p)
pNext = pNext->next;
pNext = NULL;
delete p;
p = NULL;
}
}
最后需要说明一点:本函数调用之前需要确保p是存在于链表中的。
倒数第k个节点
输入一个链表,输出该链表的倒数第k个节点,从1开始计数。
思路1:遍历得到链表长度n,再遍历找到第n-k+1个节点。
思路2:使用2个指针,第一个先走k-1步,之后两个指针一起走,直到第一个指针走到末尾(即其next为NULL)。
ListNode * findKthToTail(ListNode *head, unsigned int k)
{
if (NULL == head || 0 == k) return; //注意k=0的情况
ListNode *node1 = head, *node2 = head;
int cnt = 0;
while (node1 && cnt < k-1)
{
node1 = node1->next;
++cnt;
}
if (NULL == node1) //链表长度小于k
return NULL;
while (node1->next)
{
node1 = node1->next;
node2 = node2->next;
}
return node2;
}
反转链表
假设有3个节点: pre->cur->nxt。
我们将pre->cur反转后得到 pre<-cur nxt。
中间有断开的地方,为了下次能够访问到nxt,我们必须暂存nxt,同时,为了实现反转,我们需要访问到pre,所以也需要暂存pre。于是,为实现反转,我们需要3个指针分别指向上面三者。
需要注意一些边界情况:
链表为空,链表只有1个节点。
ListNode * reverseList(ListNode *head)
{
if (NULL == head)
return ;
ListNode *pre = NULL, *cur = head, *nxt = NULL;
ListNode *reverseHead = NULL;
while (cur)
{
nxt = cur->next;
if (NULL == nxt)
reverseHead = cur;
cur->next = pre;
pre = cur;
cur = nxt;
}
return reverseHead;
}
关于链表的题目还有很多~这里就不一一举例了~
下一次我们将学习二叉树相关的内容!
参考资料:
《数据结构(C++版)(第2版)》 -王红梅 胡明 王涛 编著
《剑指offer》 -何海涛 著
每天进步一点点,Come on!
(●’◡’●)
本人水平有限,如文章内容有错漏之处,敬请各位读者指出,谢谢!