链表应该是最重要的基本数据结构之一。本科数据结构白学了都忘光了(too naive!!),现在借助找实习的契机重新温故一下(学习学习,涨涨姿势)。
那么,链表有哪些特性呢?
链表是一种动态的数据结构,创建链表的时候无需知道它的长度。
当插入一个新的结点的时候只需要为新节点分配内存并调整指针即可。
故内存分配不是在创建时一次性完成的,而是每添加一个结点分配一次内存。
(而创建数组时需要指定数组的大小,然后根据大小分配内存,故会造成空间浪费。)
虽然链表的空间效率很高,但是时间效率却没有数组好,因为内存不是一次性分配,所以无法保证链表的内存和数组一样是连续的。因此如果想在链表中找到第i个结点,只能从头遍历链表,时间效率为O(n),而数组是O(1)。
c++定义一个单向链表的结点如下:
struct ListNode
{
int m_Value; //值
ListNode* m_pNext; //指向下一个结点的指针
}
或者
struct ListNode
{
int m_Value;
ListNode *m_pNext;
struct ListNode(int x):m_Value(x),m_pNext(NULL){}
};
链表一些常用的操作:插入,删除。
(1)在链表末尾添加一个结点:
void AddToTail(ListNode **pHead, int value)
{
ListNode* pNew = new ListNode(); // 从堆中实例化新的结点
pNew->m_Value = value;
pNew->m_pNext = NULL;
//空链表的情况,这种情况需要对pHead进行修改,故上面传参的时候需要用指向指针的指针
//否则这个函数调用完了仍然是空链表,这涉及到值传参传递的是副本
if (*pHead == NULL)
{
*pHead = pNew;
}
else
{
ListNode* pNode = *pHead;
while(pNode->m_Next != NULL)
pNode = pNode->m_pNext;
pNode->m_pNext = pNew;//在末尾添加新的结点
}
}
如果上面指向指针的指针不明白的话可以参考下面这个博客,通透!
http://blog.youkuaiyun.com/shen_jz2012/article/details/50631317
(2)删除某特定值第一次出现的结点
void RemoveNode(ListNode** pHead, int value)
{
//空链表
if(pHead == NULL || *pHead == NULL)
return;
//如果要删除的就是第一个结点,那么需要修改头指针
ListNode* pToBeDeleted = NULL;
if ((*pHead)->m_Value == value)
{
pToBeDeleted = *pHead;
*pHead = (*pHead)->m_pNext;
}
//其它结点
else
{
ListNode* pNode = *pHead;
while(pNode->m_pNext != NULL && pNode->m_pNext->m_Value != value)
pNode = pNode->m_pNext;
//如果找到那个点的前驱
if(pNode->m_pNext != NULL && pNode->m_pNext->m_Value == value)
{
pToBeDeleted = pNode->m_pNext;
pNode->m_pNext = pNode->m_pNext->m_pNext;
}
}
//删除结点
if(pToBeDeleted != NULL)
{
delete pToBeDeleted;
pToBeDeleted = NULL;
}
}
(3)给定单向链表的头指针和一个结点指针,在O(1)时间删除该结点
分析:之前删除结点,需要找到该结点的前驱结点,然后pNode->m_pNext = pNode->m_pNext->m_pNext,而因为链表的内存不像数组那样是连续的,故需要从头开始遍历,时间复杂度是O(n)。那么如何才能在O(1)时间删除一个结点呢?
其实方法很简单,我们有了一个结点的信息,其实也就知道了它下一个结点的位置,那么我们只要将下一个结点的值复制到当前结点,并让当前结点的指针指向下下一个结点即可。当然,实际编程过程中还有一些特殊情况需要考虑。
void DeleteNode(ListNode** pHead, ListNode* pToBeDelete)
{
if(!pHead || !pToBeDelete)
return;
//正常情况,不是最后一个结点
if(pToBeDelete->m_pNext != NULL)
{
//pToBeDeleted->m_Value = pToBeDeleted->m_pNext->m_Value;
//pToBeDeleted->m_pNext = pToBeDeleted->m_pNext->m_pNext;
ListNode* pNext = pToBeDeleted->m_pNext;
pToBeDeleted->m_Value = pNext->m_Value;
pToBeDeleted->m_pNext = pNext->m_pNext;
delete pNext;
pNext = NULL;
}
//如果只有一个结点,要删除的是第一个结点也即是最后一个结点,那么删除了就什么都没了,需要把头指针和指向待删除的结点的指针置空
else if(*pHead == pToBeDeleted)
{
delete pToBeDeleted;
pToBeDelete = NULL;
*pHead = NULL;
}
//如果有多个结点,但是要删除的结点是最后一个结点,那么上面方法就失效了,就只能按照从头遍历的方法
else
{
ListNode* pNode = *pHead;
while(pNode->m_pNext != pToBeDeleted)
pNode = pNode->m_pNext;
pNode->m_pNext = NULL;
delete pToBeDeleted;
pToBeDeleted = NULL;
}
}
分析一下时间复杂度:[(n-1)*O(1)+O(n)]/n = O(1).
(4) 逆序打印链表
很容易想到先进后出的堆栈
void PrintListReverse(ListNode* pHead)
{
std::stack<ListNode*> nodes;
ListNode* pNode = pHead;
while(pNode != NULL)
{
nodes.push(pNode);// 压入堆栈
pNode = pNode->m_pNext;
}
while(!nodes.empty())//若非空打印栈顶元素并弹出
{
pNode = nodes.top();//返回栈顶元素
printf("%d\t", pNode->m_Value);
nodes.pop();// 弹出
}
}
进一步想了一下,递归的代码更简洁
void PrintListReverse(ListNode* pHead)
{
if(pHead == NULL)
return;
ListNode* pNode = pHead;
if (pNode->m_Next != NULL)
PrintListReverse(pNode->m_Next);
printf("%d\t",pNode->m_Value)
}