`链表简单算法题
1.将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成。(附leetcode 21.)
//方法一
struct ListNode *mergeTwoLists(struct ListNode* l1, struct ListNode* l2){
if(!l1)
return l2;
if(!l2)
return l1;
struct ListNode *head=(struct ListNode*)malloc(sizeof(struct ListNode)),*t=head;
while(l1&&l2){
if(l1->val<l2->val){
t->next=l1;
l1=l1->next;
}
else
{
t->next=l2;
l2=l2->next;
}
t->next;
}
if(l1)
t->next=l1;
else if(l2)
t->next=l2;
return head->next;
}
//方法二:
//终止条件:当链表中有一个链表为NULL,则直接返回另外一个链表。
//递归方法:判断l1,l2头节点的大小,然后将较小的next指向其余节点的合并结果。
struct ListNode* mergeTwoLists(struct ListNode* l1, struct ListNode* l2){
if (l1 == NULL)
{
return l2;
}
if (l2 == NULL)
{
return l1;
}
if(l1->val <= l2->val)
{
l1->next = mergeTwoLists(l1->next, l2);
return l1;
}
else
{
l2->next = mergeTwoLists(l1, l2->next);
return l2;
}
}
2.给定一个头结点为 head
的非空单链表,返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点。(附leetcode 876)
//快慢指针
//思路:设立两个指针,一个一次走两步,一个一次走一步,当走两步的走到终点的时候,那个走一步的刚好走到中点
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode* middleNode(struct ListNode* head){
struct ListNode *fast = head;
struct ListNode *slow = head;
while( (fast) && (fast->next)){
slow = slow -> next;
fast = fast -> next -> next;
}
return slow;
}
3.反转一个链表(附leetcode 206)
示例:
输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL
//方法一:迭代,双指针法
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode* reverseList(struct ListNode* head){
struct ListNode* cur = head;
struct ListNode* pre = NULL;
while (cur) {
struct ListNode* tmp = cur->next;
cur->next = pre;
pre = cur;
cur = tmp;
}
return pre;
}
//方法二:头插法
//思路:通过遍历head的每一个结点,用头插法的方式插入到head上。
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode* reverseList(struct ListNode* head){
struct ListNode *p = head, *r;//p指针指向head第一个结点,r指针为防止不断链而设的
head = NULL;//令链表为空
while(p != NULL){
r = p -> next;//指向p的下一个结点,防止工作指针p头插法导致链表断链
p -> next = head;
head = p;
p = r;
}
return head;
}
//方法三:递归
struct ListNode* reverseList(struct ListNode* head){
//当只有一个节点,它的反转就是本身
if(!head || !head->next) return head;
//取出head节点,调用函数自身将head->next为头的链表反转,结果保存在res中
struct ListNode *res = reverseList(head->next);
//res的尾巴是head->next,所以需要将之前弹出的head接在此尾巴上
head->next->next = head;
head->next = NULL;
return res;
}
4.实现一种算法,删除单向链表中间的某个节点(即不是第一个或最后一个节点),假定你只能访问该节点。
输入:单向链表a->b->c->d->e->f中的节点c
结果:不返回任何数据,但该链表变为a->b->d->e->f
//思路:
因为说了只能访问当前节点,所以不能简单的 前一个node->next = 当前node->next;free(node);
那么只能让 当前node = 后一个node,然后free(后一个node);
代码如下,记得别忘了free()哦
内存泄露,可没有你好果汁吃!!!
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
void deleteNode(struct ListNode* node) {
struct ListNode* temp = node->next;
node->val = node->next->val;//实现值的交换
node->next = node->next->next;
free(temp);
}
5.给你一个单链表的引用结点 head。链表中每个结点的值不是 0 就是 1。已知此链表是一个整数数字的二进制表示形式。
输入:head = [1,0,1]
输出:5
解释:二进制数 (101) 转化为十进制数 (5)
提示:
链表不为空。
链表的结点总数不超过 30。
每个结点的值不是 0 就是 1。
方法一:
设置一个计数器,首先遍历整个链表,通过计数器的值知道有多少个节点。然后重新遍历链表,此次遍历主要用来二进制到十进制的转换,根据之前的计数器的值作为各权位的幂,即可求出。
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
int getDecimalValue(struct ListNode* head){
int j=0;
int sum=0;
struct ListNode *p;
p=head;
while(p!=NULL)
{
p=p->next;
j++;
}
p=head;
while(p!=NULL&&j>0)
{
sum+=((p->val)*pow(2,j-1));
j--;
p=p->next;
}
return sum;
}
方法二:
由于链表中从高位到低位存放了数字的二进制表示,因此我们可以使用二进制转十进制的方法,在遍历一遍链表的同时得到数字的十进制值。
int getDecimalValue(struct ListNode* head){
struct ListNode *p=head;
int ans=0;
while(p!=NULL){
ans=ans*2+p->val;
p=p->next;
}
return ans;
}
6. 存在一个按升序排列的链表,给你这个链表的头节点 head
,请你删除所有重复的元素,使每个元素 只出现一次。返回同样按升序排列的结果链表。
方法一:一次遍历
思路:
由于给定的链表是排好序的,因此重复的元素在链表中出现的位置是连续的,因此我们只需要对链表进行一次遍历,就可以删除重复的元素。
具体地,我们从指针cur 指向链表的头节点,随后开始对链表进行遍历。如果当前 cur 与 cur.next 对应的元素相同,那么我们就将 cur.next 从链表中移除;否则说明链表中已经不存在其它与 cur 对应的元素相同的节点,因此可以将cur 指向 cur.next。
当遍历完整个链表之后,我们返回链表的头节点即可。
struct ListNode* deleteDuplicates(struct ListNode* head) {
if (!head) {
return head;
}
struct ListNode* cur = head;
while (cur->next) {
if (cur->val == cur->next->val) {
cur->next = cur->next->next;
} else {
cur = cur->next;
}
}
return head;
}
7.给定一个链表,判断链表中是否有环。
如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。
如果链表中存在环,则返回 true 。 否则,返回 false 。
方法一:快慢指针
思路:定义两个指针,一快一满。慢指针每次只移动一步,而快指针每次移动两步。初始时,慢指针在位置 head,而快指针在位置 head.next。这样一来,如果在移动的过程中,快指针反过来追上慢指针,就说明该链表为环形链表。否则快指针将到达链表尾部,该链表不为环形链表。
bool hasCycle(struct ListNode* head) {
if (head == NULL || head->next == NULL) {
return false;
}
struct ListNode* slow = head;
struct ListNode* fast = head->next;
while (slow != fast) {
if (fast == NULL || fast->next == NULL) {
return false;
}
slow = slow->next;
fast = fast->next->next;
}
return true;
}
8. 请判断一个链表是否为回文链表。(附leetcode .234)
示例 :
输入: 1->2
输出: false
输入: 1->2->2->1
输出: true
方法一:将值复制到数组中后用双指针法
思路:
确定数组列表是否回文很简单,我们可以使用双指针法来比较两端的元素,并向中间移动。一个指针从起点向中间移动,另一个指针从终点向中间移动。这需要 O(n)O(n) 的时间,因为访问每个元素的时间是 O(1)O(1),而有 nn 个元素要访问。
然而同样的方法在链表上操作并不简单,因为不论是正向访问还是反向访问都不是 O(1)O(1)。而将链表的值复制到数组列表中是 O(n)O(n),因此最简单的方法就是将链表的值复制到数组列表中,再使用双指针法判断。
一共为两个步骤:
1.复制链表值到数组列表中。
2.使用双指针法判断是否为回文。
第一步,我们需要遍历链表将值复制到数组列表中。我们用 currentNode 指向当前节点。每次迭代向数组添加 currentNode.val,并更新 currentNode = currentNode.next,当 currentNode = null 时停止循环。
执行第二步的最佳方法取决于你使用的语言。在 Python 中,很容易构造一个列表的反向副本,也很容易比较两个列表。而在其他语言中,就没有那么简单。因此最好使用双指针法来检查是否为回文。我们在起点放置一个指针,在结尾放置一个指针,每一次迭代判断两个指针指向的元素是否相同,若不同,返回 false;相同则将两个指针向内移动,并继续判断,直到两个指针相遇。
在编码的过程中,注意我们比较的是节点值的大小,而不是节点本身。正确的比较方式是:node_1.val == node_2.val,而 node_1 == node_2 是错误的。
bool isPalindrome(struct ListNode* head) {
int vals[50001], vals_num = 0;
while (head != NULL) {
vals[vals_num++] = head->val;
head = head->next;
}
for (int i = 0, j = vals_num - 1; i < j; ++i, --j) {
if (vals[i] != vals[j]) {
return false;
}
}
return true;
}
方法三:快慢指针
思路:
避免使用 O(n)O(n) 额外空间的方法就是改变输入。
我们可以将链表的后半部分反转(修改链表结构),然后将前半部分和后半部分进行比较。比较完成后我们应该将链表恢复原样。虽然不需要恢复也能通过测试用例,但是使用该函数的人通常不希望链表结构被更改。
该方法虽然可以将空间复杂度降到 O(1)O(1),但是在并发环境下,该方法也有缺点。在并发环境下,函数运行时需要锁定其他线程或进程对链表的访问,因为在函数执行过程中链表会被修改。
算法
整个流程可以分为以下五个步骤:
找到前半部分链表的尾节点。
反转后半部分链表。
判断是否回文。
恢复链表。
返回结果。
执行步骤一,我们可以计算链表节点的数量,然后遍历链表找到前半部分的尾节点。
我们也可以使用快慢指针在一次遍历中找到:慢指针一次走一步,快指针一次走两步,快慢指针同时出发。当快指针移动到链表的末尾时,慢指针恰好到链表的中间。通过慢指针将链表分为两部分。
若链表有奇数个节点,则中间的节点应该看作是前半部分。
步骤二可以使用「206. 反转链表」问题中的解决方法来反转链表的后半部分。
步骤三比较两个部分的值,当后半部分到达末尾则比较完成,可以忽略计数情况中的中间节点。
步骤四与步骤二使用的函数相同,再反转一次恢复链表本身。
struct ListNode* reverseList(struct ListNode* head) {
struct ListNode* prev = NULL;
struct ListNode* curr = head;
while (curr != NULL) {
struct ListNode* nextTemp = curr->next;
curr->next = prev;
prev = curr;
curr = nextTemp;
}
return prev;
}
struct ListNode* endOfFirstHalf(struct ListNode* head) {
struct ListNode* fast = head;
struct ListNode* slow = head;
while (fast->next != NULL && fast->next->next != NULL) {
fast = fast->next->next;
slow = slow->next;
}
return slow;
}
bool isPalindrome(struct ListNode* head) {
if (head == NULL) {
return true;
}
// 找到前半部分链表的尾节点并反转后半部分链表
struct ListNode* firstHalfEnd = endOfFirstHalf(head);
struct ListNode* secondHalfStart = reverseList(firstHalfEnd->next);
// 判断是否回文
struct ListNode* p1 = head;
struct ListNode* p2 = secondHalfStart;
bool result = true;
while (result && p2 != NULL) {
if (p1->val != p2->val) {
result = false;
}
p1 = p1->next;
p2 = p2->next;
}
// 还原链表并返回结果
firstHalfEnd->next = reverseList(secondHalfStart);
return result;
}
9.给你一个链表的头节点 head
和一个整数 val
,请你删除链表中所有满足 Node.val == val
的节点,并返回 新的头结点。(附lettcode.203)
方法二:迭代
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode* removeElements(struct ListNode* head, int val){
/* 找到第一个不为目标值的节点 */
while (head != NULL && head->val == val) {
head = head->next;
}
/* 若都是目标值,则返回NULL */
if (head == NULL) {
return NULL;
}
/* 用于存储上一个节点的值 */
struct ListNode* pre = head;
while (pre->next != NULL) {
if (pre->next->val == val) {
pre->next = pre->next->next;
} else {
pre = pre->next;
}
}
return head;
}