链表定义及使用方法:
链表是一种数据结构,在内存中通过节点记录内存地址而相互链接形成一条链的储存方式。相比数组而言,链表在内存中不需要连续的区域,只需要每一个节点都能够记录下一个节点的内存地址,通过引用进行查找,这样的特点也就造就了链表增删操作时间消耗很小,而查找遍历时间消耗很大的特点。
链表是由其基本组成单元节点(Node)来实现的。我们在日常中见到的链表大部分都是单链表和双链表,这两种链表在实现思维上基本一致,只不过在插入、删除等操作实现上有所区别。
两种链表结构如图所示:
从图中可以看出,二者主要差别在于内部的Node类。单链表只需要一个指向下一个节点的引用Next,而双向链表则需要指向前一个Node的prev和下一个Node的Next。
单链表
在我们实现一个链表时,一定要注意引用的使用,如果一旦引用指向空,很可能再也取不到这些数据。因此,如果不确定自己到底需要几个引用来指向不同的节点来实现不同的需求,可以多创建几个节点。
按照习惯,我一般会创建三个引用用于链表的各项操作。
theHeadPointer 头引用,始终指向链表头部,用于遍历链表,在头部增删数据等操作。theLastPointer 尾引用,始终指向链表尾部,用于在尾部新增数据,同时可以用于删除数据。
theCurrentPointer 当前引用(也称哨兵引用),使用较为灵活,可以在操作数据时作为保险。
下图为单链表的数据操作步骤。
双向链表
二者最大的不同就是引用指向不同。实际上,在日常中,双向链表应用更加广泛,相比单链表,双向链表可从头尾两端进行遍历的特点非常具有优势,当我们需要查找最新的数据时,我们可从尾部开始遍历,需要查找旧数据时,从头部开始遍历,这样能大大减少遍历链表所需要的昂贵的花费。而单链表则只能从头部开始遍历。
下图为双向链表的操作.
双向链表的添加删除操作实现原理基本相同,只是在我们删除前,一定要保证删除节点的后一个节点有引用,否则很容易再也无法获得后面数据,为了防止这种情况,可以多创建几个引用,以防万一。
public class DoubleLinkedListNode {
//数据
private Object element;
// 前指针
private DoubleLinkedListNode pre;
// 后指针
private DoubleLinkedListNode next;
public DoubleLinkedListNode(DoubleLinkedListNode pre, Object element, DoubleLinkedListNode next) {
this.element = element;
this.pre = pre;
this.next = next;
}
}
链表的时间复杂度分析
添加操作 O(n)
addLast(e) O(n)
addFirst(e) O(1)
add(index,e) O(n/2)=O(n)
删除操作 O(n)
removeLast(e) O(n)
removeFirst(e) O(1)
remove(index,e) O(n/2)=O(n)
修改操作 O(n)
set(index,e) O(n)
查找操作 O(n)
get(index) O(n)
contains(e) O(n)
链表的增删改查的时间复杂度都是O(n)
不过增删操作如果只对链表头进行操作时间复杂度是O(1)
查如果只查链表头的元素时间复杂度也是O(1)
链表部分题目及代码学习梳理
输出倒数第k个结点:
ListNode* findKthTail(ListNode *pHead, int K){
if (NULL == pHead || K == 0)
return NULL;
//p1,p2均指向头节点
ListNode *p1 = pHead;
ListNode *p2 = pHead;
//p1先出发,前进K个节点
for (int i = 0; i < K; i++) {
if (p1)//防止k大于链表节点的个数
p1 = p1->_next;
else
return NULL;
}
while (p1)//如果p1没有到达链表结尾,则p1,p2继续遍历
{
p1 = p1->_next;
p2 = p2->_next;
}
return p2;//当p1到达末尾时,p2正好指向倒数第K个节点
}
链表中是否有环
bool isExistLoop(ListNode* pHead) {
ListNode* fast;//慢指针,每次前进一个节点
ListNode* slow;//快指针,每次前进2个节点
slow = fast = pHead ; //两个指针均指向链表头节点
//当没有到达链表结尾,则继续前进
while (slow != NULL && fast -> next != NULL) {
slow = slow -> next ; //慢指针前进一个节点
fast = fast -> next -> next ; //快指针前进两个节点
if (slow == fast) //若两个指针相遇,且均不为NULL则存在环
return true ;
}
//到达末尾仍然没有相遇,则不存在环
return false ;
}
链表中环的起点:
//找到环中的相遇节点
ListNode* getMeetingNode(ListNode* pHead) // 假设为带头节点的单链表
{
ListNode* fast;//慢指针,每次前进一个节点
ListNode* slow;//快指针,每次前进2个节点
slow = fast = pHead ; //两个指针均指向链表头节点
//当没有到达链表结尾,则继续前进
while (slow != NULL && fast -> next != NULL){
slow = slow -> next ; //慢指针前进一个节点
fast = fast -> next -> next ; //快指针前进两个节点
if (slow == fast) //若两个指针相遇,且均不为NULL则存在环
return slow;
}
//到达末尾仍然没有相遇,则不存在环
return NULL ;
}
//找出环的入口节点
ListNode* getEntryNodeOfLoop(ListNode* pHead){
ListNode* meetingNode = getMeetingNode(pHead); // 先找出环中的相遇节点
if (meetingNode == NULL)
return NULL;
ListNode* p1 = meetingNode;
ListNode* p2 = pHead;
while (p1 != p2) // p1和p2以相同的速度向前移动,当p2指向环的入口节点时,p1已经围绕着环走了n圈又回到了入口节点。
{
p1 = p1->next;
p2 = p2->next;
}
//返回入口节点
return p1;
}
计算环长度
int getLoopLength(ListNode* head){
ListNode* slow = head;
ListNode* fast = head;
while ( fast && fast->next ){
slow = slow->next;
fast = fast->next->next;
if ( slow == fast )//第一次相遇
break;
}
//slow与fast继续前进
slow = slow->next;
fast = fast->next->next;
int length = 1; //环长度
while ( fast != slow )//再次相遇
{
slow = slow->next;
fast = fast->next->next;
length ++; //累加
}
//当slow与fast再次相遇,得到环长度
return length;
}
反转链表
如:
示例:
输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL
进阶:
你可以迭代或递归地反转链表。你能否用两种方法解决这道题?
8.2 解题思路
设置三个节点pre
、cur
、next
-
(1)每次查看
cur
节点是否为NULL
,如果是,则结束循环,获得结果 -
(2)如果
cur
节点不是为NULL
,则先设置临时变量next
为cur
的下一个节点 -
(3)让
cur
的下一个节点变成指向pre
,而后pre
移动cur
,cur
移动到next
-
(4)重复(1)(2)(3)
8.3 动画演示
8.4 代码实现
8.4.1 迭代方式
class Solution {
public:
ListNode* reverseList(ListNode* head) {
ListNode* pre = NULL;
ListNode* cur = head;
while(cur != NULL){
ListNode* next = cur->next;
cur->next = pre;
pre = cur;
cur = next;
}
return pre;
}
};
8.4.2 递归的方式处理
class Solution {
public:
ListNode* reverseList(ListNode* head) {
// 递归终止条件
if(head == NULL || head->next == NULL)
return head;
ListNode* rhead = reverseList(head->next);
// head->next此刻指向head后面的链表的尾节点
// head->next->next = head把head节点放在了尾部
head->next->next = head;
head->next = NULL;
return rhead;
}
};
使用链表实现栈
如:
使用链表实现栈
如: