目录
1. 线性表
2. 顺序表
2.1 概念及结构
顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数据的增删查改。顺序表的物理结构和逻辑结构是一致的。
顺序表一般可以分为:
静态顺序表:
//静态的顺序表,并不实用
#define N 100
struct SeqList
{
int a[N];
int size;
};
动态顺序表:
//顺序表的动态存储
struct SeqList
{
int* a;
int size; //表示a中有多少个有效数据
int capacity; //表示a的空间到底有多大
};
此时的顺序表有一点问题,只能存 int 类型的变量,如果要修改会比较麻烦,所以我们这样改善:
//顺序表的动态存储
typedef int SeqDataType; //如果要修改则只需要修改这一处
typedef struct SeqList
{
SeqDataType* a;
int size; //表示a中有多少个有效数据
int capacity; //表示a的空间到底有多大
}SeqList, SEQ;
2.2 接口实现
静态顺序表只适用于确定知道需要存多少数据的场景。静态顺序表的定长数组导致N定大了,空间开辟多了浪费,开辟少了不够用。所以现实中基本都是使用动态顺序表,根据需要动态的分配空间大小,所以下面我们实现动态顺序表。
Seqlist.h
#pragma once
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
静态的顺序表,并不实用
//#define N 100
//struct SeqList
//{
// int a[N];
// int size;
//};
//顺序表的动态存储
typedef int SeqDataType; //如果要修改则只需要修改这一处
typedef struct SeqList
{
SeqDataType* a;
int size; //表示a中有多少个有效数据
int capacity; //表示a的空间到底有多大
}SeqList, SEQ;
//增删查改
//尾插
void SeqListPushBack(SeqList* pq, SeqDataType x);
//头插
void SeqListPushFront(SeqList* pq, SeqDataType x);
//头删
void SeqListPopFront(SeqList* pq);
//尾删
void SeqListPopBack(SeqList* pq);
//查找
int SeqListFind(SeqList* pq, SeqDataType x);
//修改
void SeqListModify(SeqList* pq, int pos, SeqDataType x);
//中间位置插入
void SeqListInsert(SeqList* pq, int pos, SeqDataType x);
//中间位置删除
void SeqListErase(SeqList* pq, int pos);
//初始化
void SeqListInit(SeqList *pq);
//销毁
void SeqListDestory(SeqList *pq);
//打印
void SeqListPrint(SeqList* pq);
//扩容
void SeqCheckCapacity(SeqList* pq);
SeqList.c
#include "SeqList.h"
//初始化
void SeqListInit(SeqList *pq)
{
assert(pq);
pq->a = NULL;
pq->size = pq->capacity = 0;
}
//销毁
void SeqListDestory(SeqList *pq)
{
free(pq->a);
pq->a = NULL;
pq->capacity = pq->size = 0;
}
//扩容
void SeqCheckCapacity(SeqList* pq)
{
//如果满了,需要增容
if (pq->size == pq->capacity)
{
//int newcapacity = pq->capacity * 2; //这样会存在问题,当一开始插入数据时,capacity为0,乘2后还是为0
int newcapacity = pq->capacity == 0 ? 4 : pq->capacity * 2; //如果capacity等于0了,则开辟4个空间
SeqDataType* newA = realloc(pq->a, sizeof(SeqDataType*) * newcapacity);
if (newA == NULL)
{
printf("realloc fail\n");
exit(-1);
}
pq->a = newA;
pq->capacity = newcapacity;
}
}
//尾插
void SeqListPushBack(SeqList* pq, SeqDataType x)
{
assert(pq);
//如果满了,需要增容
SeqCheckCapacity(pq);
pq->a[pq->size] = x;
pq->size++;
}
//头插
void SeqListPushFront(SeqList* pq, SeqDataType x)
{
assert(pq);
//如果满了,需要增容
SeqCheckCapacity(pq);
//头插操作
int end = pq->size - 1;
while (end >= 0) //当end = -1时终止
{
//前面的元素以此向后移动
pq->a[end + 1] = pq->a[end];
end--;
}
pq->a[0] = x;
pq->size++;
}
//尾删
void SeqListPopBack(SeqList* pq)
{
assert(pq);
assert(pq->size > 0); //如果size为0了,没有元素了,谈何删除
pq->size--;
//注意,不能把pq->size置0,这不叫删除,且万一该数据就是0呢
}
//头删
void SeqListPopFront(SeqList* pq)
{
assert(pq);
assert(pq->size > 0);
int begin = 0;
//从下标1开始依次前移一个
while (begin < pq->size - 1)
{
pq->a[begin] = pq->a[begin + 1];
begin++;
}
pq->size--;
}
//查找
int SeqListFind(SeqList* pq, SeqDataType x)
{
assert(pq);
int i = 0;
for (i = 0; i < pq->size; i++)
{
if (pq->a[i] == x)//若查找成功,返回下标
return i;
}
return -1;
}
//修改
void SeqListModify(SeqList* pq, int pos, SeqDataType x)
{
assert(pq);
assert(pos >= 0 && pos < pq->size);
pq->a[pos] = x;
}
//中间位置插入
void SeqListInsert(SeqList* pq, int pos, SeqDataType x)
{
assert(pq);
assert(pos >= 0 && pos <= pq->size);
//当pos = 0时 => 头插
//当pos = size时 => 尾插
//如果已满,需要增容
SeqCheckCapacity(pq);
//把pos后面的数全部前移
int end = pq->size - 1;
while (end >= pos)
{
pq->a[end + 1] = pq->a[end];
end--;
}
//pos处放入x
pq->a[pos] = x;
pq->size++;
}
//中间位置删除
void SeqListErase(SeqList* pq, int pos)
{
assert(pq);
assert(pos >= 0 && pos < pq->size);
//当 pos = size - 1 => 尾删
// pos = 0 => 头删
int begin = pos;
while (begin < pq->size - 1)
{
pq->a[begin] = pq->a[begin + 1];
begin++;
}
pq->size--;
}
//打印
void SeqListPrint(SeqList* pq)
{
assert(pq);
for (int i = 0; i < pq->size; ++i)
{
printf("%d ", pq->a[i]);
}
}
test.c
#include "SeqList.h"
//测试头插、尾插、头删、尾删
void TestSeqList1()
{
SeqList s;
SeqListInit(&s);
SeqListPushBack(&s, 1);
SeqListPushBack(&s, 2);
SeqListPushBack(&s, 3);
SeqListPushBack(&s, 4);
SeqListPushBack(&s, 5);
//SeqListPrint(&s); //1 2 3 4 5
SeqListPushFront(&s, 0);
//SeqListPrint(&s); //0 1 2 3 4 5
SeqListPopBack(&s);
//SeqListPrint(&s); //0 1 2 3 4
SeqListPopFront(&s);
//SeqListPrint(&s); //1 2 3 4
SeqListDestory(&s);
}
//测试中间插入、删除、修改
void TestSeqList2()
{
SeqList s;
SeqListInit(&s);
SeqListPushBack(&s, 1);
SeqListPushBack(&s, 2);
SeqListPushBack(&s, 3);
SeqListPushBack(&s, 4);
SeqListPushBack(&s, 5);
SeqListInsert(&s, 2, 30);
//SeqListPrint(&s);//1 2 30 3 4 5
SeqListErase(&s, 0);
//SeqListPrint(&s);//2 30 3 4 5
SeqListModify(&s, 1, 3);
SeqListPrint(&s);//2 3 3 4 5
SeqListDestory(&s);
}
int main()
{
TestSeqList2();
return 0;
}
2.3 数组相关面试题
面试题1:移除元素
题目描述:
给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并原地修改输入数组。元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。
int removeElement(int* nums, int numsSize, int val)
{
int src = 0;
int dst = 0;
while(src != numsSize)
{
if(nums[src] != val)
{
nums[dst] = nums[src];
dst++;
src++;
}
else
{
src++;
}
}
return dst;
}
面试题2:删除有序数组中的重复项
题目描述:
给你一个升序排列的数组 nums ,请你 原地 删除重复出现的元素,使每个元素 只出现一次 ,返回删除后数组的新长度。元素的 相对顺序 应该保持 一致 。
如果在删除重复项之后有 k 个元素,那么 nums 的前 k 个元素应该保存最终结果。将最终结果插入 nums 的前 k 个位置后返回 k 。
不要使用额外的空间,你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成。
int removeDuplicates(int* nums, int numsSize)
{
//如果是空数组,直接返回0
if(numsSize== 0)
return 0;
int cur = 0;
int dst = 0;
int next = 1;
while(next < numsSize)
{
if(nums[cur] != nums[next])//前后不相等,直接把值赋给dst
{
nums[dst++] = nums[cur++];
next++;
}
else
{
//前后相等时,next继续往后找,直到两者不相等为止
//在该循环也需要限制next范围,否则容易出现越界
while(next < numsSize && nums[cur] == nums[next])
{
next++;
}
nums[dst] = nums[cur];
dst++;
cur = next; //cur跳过已经判断出相等的部分
next++;
}
}
if(cur < numsSize)//如果不进行判断,可能会出现越界访问
{
nums[dst] = nums[cur]; //把最后一个值赋给dst
dst++;
}
return dst;
}
面试题3:合并两个有序数组
题目描述:给你两个按 非递减顺序 排列的整数数组 nums1 和 nums2,另有两个整数 m 和 n ,分别表示 nums1 和 nums2 中的元素数目(nums1Size、numsSize2分别表示nums1、nums2当前的空间大小)。
请你 合并 nums2 到 nums1 中,使合并后的数组同样按 非递减顺序 排列。
注意:最终,合并后数组不应由函数返回,而是存储在数组 nums1 中。为了应对这种情况,nums1 的初始长度为 m + n,其中前 m 个元素表示应合并的元素,后 n 个元素为 0 ,应忽略。nums2 的长度为 n 。
void merge(int* nums1, int nums1Size, int m, int* nums2, int nums2Size, int n)
{
//两个变量分别指向两个数组的最后一个元素
int end1 = m - 1;
int end2 = n - 1;
int end = m + n - 1; //合并成一个数组的最后一个元素下标
while(end1 >= 0 && end2 >= 0)
{
if(nums1[end1] > nums2[end2])
{
nums1[end--] = nums1[end1--];
}
else
{
nums1[end--] = nums2[end2--];
}
}
//循环结束后:
//1.如果end2还没结束,说明nums2中还有数据。需要把nums2剩余数据继续挪动到nums1中
//2.如果end1还没结束,不需要挪动了,因为本身就在nums1中从后往前操作
while(end2 >= 0)
{
nums1[end--] = nums2[end2--];
}
}
面试题4:旋转数组
题目描述:给你一个数组,将数组中的元素向右轮转 k
个位置,其中 k
是非负数。
void reverse(int* arr, int begin, int end)
{
while(begin < end)
{
int tmp = arr[begin];
arr[begin] = arr[end];
arr[end] = tmp;
begin++;
end--;
}
}
//思路:
//1.对原数组的后k个数逆置
//2.对原数组的前n-k个数逆置
//3.对原数组整体逆置
void rotate(int* nums, int numsSize, int k)
{
//如果所给k的长度超过了元素个数,则取模,没有超过,取模也不会影响结果
k %= numsSize;
int left = 0;
int right = numsSize - 1;
reverse(nums, numsSize-k, right);
reverse(nums, left, numsSize-k-1);
reverse(nums, left, right);
}
*面试题5:. 数组形式的整数加法
题目描述:整数的 数组形式 num 是按照从左到右的顺序表示其数字的数组。
例如,对于 num = 1321 ,数组形式是 [1,3,2,1] 。
给定 num ,整数的 数组形式 ,和整数 k ,返回 整数 num + k 的 数组形式 。
int* addToArrayForm(int* num, int numSize, int k, int* returnSize){
int kSize = 0;//用于计算k有多少位
int number = k;
while(number)
{
kSize++;
number /= 10;
}
//需要的长度 等于k与数组长度的最大值再+1(因为可能有进位的情况)
int len = kSize > numSize ? kSize+1 : numSize+1;
int *retArr = (int *)malloc(sizeof(int) * len);
int Ni = numSize - 1; //数组转整数的"个位"
int Ki = 0;
int next = 0; //进位,默认为0
int reti = 0; //加法结束后,依次放入数组的位置
while(Ni >=0 || Ki < kSize) //必须都数完毕,循环才结束
{
int nVal = 0;
if(Ni >= 0)//在Ni在合法范围时,取出数组中的值
{
nVal = num[Ni];
}
int kVal = k % 10;
k /= 10;
int ret = nVal + kVal + next;
if(ret >= 10)//进位了
{
next = 1;
ret -= 10;//比如进位后是15,则只留下5
}
else
{
next = 0;
}
//将ret放入数组中
//从数组最后(个位)往前放面临的问题
//因为之前为了满足进位的情况,多开辟了一个空间
//如果最后一位不进位,那么多出来的空间又是num[0]的位置,会很尴尬
//所以我们选择从数组前向后放,最后再逆置数组即可
retArr[reti++] = ret;
Ni--;
Ki++;
}
//如果最后一次进位了,在数组末尾放一个1
if(next == 1)
{
retArr[reti++] = 1;
}
//逆置数组
int begin = 0;
int end = reti - 1;
while(begin < end)
{
int tmp = retArr[begin];
retArr[begin] = retArr[end];
retArr[end] = tmp;
begin++;
end--;
}
*returnSize = reti;
return retArr;
}
2.4 顺序表的缺陷
① 中间/头部的插入删除,时间复杂度为O(N)
② 增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗。
③ 增容一般是呈2倍的增长,势必会有一定的空间浪费。例如当前容量为100,满了以后增容到200,我们再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间。
如何解决以上问题呢?下面介绍链表的概念。
3. 链表
3.1 链表的概念及结构
概念:链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的 。链表的逻辑结构和物理结构是不一致的。
实际中链表的结构非常多样,以下情况组合起来就有8种链表结构:
1. 单向、双向 2. 带头、不带头 3. 循环、非循环
虽然有这么多的链表的结构,但是我们实际中最常用还是两种结构:
3.2 链表的实现
Slist.h
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
//方便存储其他类型
typedef int SLTDataType;
//定义一个链表结点
typedef struct SListNode
{
SLTDataType data; //数据域
struct SListNode* next; //指针域
}SLTNode;
//单向+不带头+循环
//尾插
void SListPushBack(SLTNode** plist, SLTDataType x);
//头插
void SListPushFront(SLTNode** pplist, SLTDataType x);
//尾删
void SListPopBack(SLTNode** pplist);
//头删
void SListPopFront(SLTNode** pplist);
//查找
SLTNode* SListFind(SLTNode* plist, SLTDataType x);
//单链表在pos位置之后插入
void SListInsertAfter(SLTNode* pos, SLTDataType x);
//并不建议在之前插入,麻烦
//单链表在pos位置之前插入(一般直接写Insert就是在之前插入)
void SListInsert(SLTNode** plist, SLTNode* pos, SLTDataType x);
//删除pos后的一个结点
void SListEraseAfter(SLTNode* pos);
//打印
void SListPrint(SLTNode* plist);
SList.c
#pragma once
#include "SList.h"
//打印
void SListPrint(SLTNode* plist)
{
SLTNode* cur = plist;
while (cur != NULL)
{
printf("%d->", cur->data);
cur = cur->next;
}
printf("NULL\n");
}
//创建一个结点
SLTNode* BuySLTNode(SLTDataType x)
{
SLTNode* node = (SLTNode*)malloc(sizeof(SLTNode));
node->data = x;
node->next = NULL;
return node;
}
//尾插
void SListPushBack(SLTNode** pplist, SLTDataType x)
//为什么是**pplist?因为我们要改变的是一级指针plist,所以传参进来应该是一个二级指针(传址调用)
{
//创建一个新节点
SLTNode* newnode = BuySLTNode(x);
//如果链表为空,让尾插的结点变成该表的第一个元素
if (*pplist == NULL)
{
*pplist = newnode;
}
else //非空, 直接尾插
{
//先遍历,找尾结点
SLTNode* tail = *pplist;
while (tail->next != NULL)
{
tail = tail->next;
}
tail->next = newnode;
//该新结点定义时就指向NULL了,所以让tail的next指向该结点即可
}
}
//头插
void SListPushFront(SLTNode** pplist, SLTDataType x)
{
//如果传入的是空指针也可以正常插入
SLTNode* newnode = BuySLTNode(x);
newnode->next = *pplist;
*pplist = newnode;
}
//尾删
void SListPopBack(SLTNode** pplist)
{
//1.没有结点的情况
if (*pplist == NULL)
{
return;
}
//2.只有一个结点的情况
else if ((*pplist)->next == NULL)
{
free(*pplist);
*pplist = NULL;
}
//3.多个结点的情况
else
{
//前驱指针
SLTNode* prev = NULL;
SLTNode* tail = *pplist;
while (tail->next != NULL)
{
//找尾结点的前驱结点
prev = tail;
//找尾结点
tail = tail->next;
}
free(tail);
tail = NULL;
prev->next = NULL;
}
}
//头删
void SListPopFront(SLTNode** pplist)
{
//1.没有结点的情况
if (*pplist == NULL)
{
return;
}
//2.多个结点的情况
else
{
//存一份下一个元素的地址(否则free过后就找不到了)
SLTNode* next = (*pplist)->next; //需要加括号,因为*和->优先级相同
free(*pplist);
*pplist = next;
}
}
//查找
SLTNode* SListFind(SLTNode* plist, SLTDataType x)
{
SLTNode* cur = plist;
//while (cur != NULL)
while(cur)
{
if (cur->data == x)
return cur;
else
cur = cur->next;
}
return NULL;
}
//单链表在pos位置之后插入
void SListInsertAfter(SLTNode* pos, SLTDataType x)
{
assert(pos);
SLTNode* newnode = BuySLTNode(x);
//注意顺序
newnode->next = pos->next;
pos->next = newnode;
}
//单链表在pos位置之前插入 (麻烦,不建议使用)
void SListInsert(SLTNode** pplist, SLTNode* pos, SLTDataType x)
{
assert(pos);
SLTNode* newnode = BuySLTNode(x);
//在第一个位置插入 => 相当于头插
if (pos == *pplist)
{
newnode->next = pos;
*pplist = newnode;
}
else
{
SLTNode* prev = NULL; //前驱
SLTNode* cur = *pplist;
while (cur != pos)
{
prev = cur;
cur = cur->next;
}
prev->next = newnode;
newnode->next = pos;
}
}
//如果需要在一个无头单链表的某一个结点的前面插入一个值x,如何插入?
//思路:先后插,然后跟他前一个结点的值(data)进行交换即可
//删除pos后的一个结点
void SListEraseAfter(SLTNode* pos)
{
assert(pos);
//1.pos后面没结点了
if (pos->next == NULL)
{
return;
}
else
{
SLTNode* next = pos->next;
pos->next = next->next;
free(next);
next = NULL;
}
}
test.c
#include "SList.h"
//尾插、头插、头删、尾删
void TestSList1()
{
SLTNode * plist = NULL;
SListPushBack(&plist, 1);
SListPushBack(&plist, 2);
SListPushBack(&plist, 3);
SListPushBack(&plist, 4);
//SListPrint(plist);//1->2->3->4->NULL
SListPushFront(&plist, 0);
//SListPrint(plist);//0->1->2->3->4->NULL
SListPopBack(&plist);
//SListPrint(plist);//0->1->2->3->NULL
SListPopFront(&plist);
SListPrint(plist);//1->2->3->NULL
}
//查找
void TestSList2()
{
SLTNode* plist = NULL;
SListPushBack(&plist, 1);
SListPushBack(&plist, 2);
SListPushBack(&plist, 3);
SListPushBack(&plist, 4);
SLTNode* pos = SListFind(plist, 3);
if (pos != NULL)
{
printf("找到了\n");
}
else
{
printf("没找到\n");
}
//找到了
//同时,可以通过pos对该位置的值进行修改
pos->data = 10;
SListPrint(plist);//1->2->10->4->NULL
}
//在pos之后、之前插入结点,删除pos后的结点
void TestSList3()
{
SLTNode* plist = NULL;
SListPushBack(&plist, 1);
SListPushBack(&plist, 2);
SListPushBack(&plist, 3);
SListPushBack(&plist, 4);
SLTNode* pos = SListFind(plist, 3);
SListInsertAfter(pos, 30);
//SListPrint(plist); //1->2->3->30->4->NULL
SListInsert(&plist, pos, 300);
//SListPrint(plist); //1->2->300->3->30->4->NULL
SListEraseAfter(pos);
//SListPrint(plist); //1->2->300->3->4->NULL
}
int main()
{
TestSList3();
return 0;
}
3.3 链表面试题
面试题1. 移除链表元素
题目描述:给你一个链表的头节点 head
和一个整数 val
,请你删除链表中所有满足 Node.val == val
的节点,并返回 新的头节点 。【注 : 若未明确说明,题目所给单链表都是不带头单链表】
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode* removeElements(struct ListNode* head, int val)
{
struct ListNode* cur = head;
struct ListNode* prev = NULL;
while(cur)
{
if(cur->val == val)
{
struct ListNode* next = cur->next;
//如果第一个元素就需要删除,则需要单独处理
//该情况下,cur是头
if(prev == NULL)
{
//删除该结点, cur和head指向下一个结点
free(cur);
head = next;
cur = next;
}
else
{
//删除该结点
prev->next = next;
free(cur);
//cur指向被删除结点的下一个结点
cur = next;
}
}
else
{
prev = cur;
cur = cur->next;
}
}
return head;
}
面试题2. 反转链表
题目描述:给你单链表的头节点 head
,请你反转链表,并返回反转后的链表。
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
//思路一:直接使用三个指针翻转
//eg:1->2->3->4->NULL
//=> NULL<- 1 <- 2 <- 3 <- 4
//最开始 : n1指向NULL,n2指向头,n3指向头的下一个结点
//每一次都让n2指向n1(n3用于记录下次n2的位置),再挪动n1、n2、n3
//当n2为空时循环结束(画图理解),此时的n1就是反转链表的头
struct ListNode* reverseList(struct ListNode* head)
{
//如果是空链表或者只有一个结点,直接返回
if(head == NULL || head->next == NULL)
return head;
struct ListNode* n1 = NULL;
struct ListNode* n2 = head;
struct ListNode* n3 = head->next;
while(n2)
{
//翻转
n2->next = n1;
//迭代
n1 = n2;
n2 = n3;
if(n3)//当执行到最后一次时,n3已经为空,如果继续执行n3->next就会出错,所以需要做判断
n3 = n3->next;
}
return n1;
}
//思路二: 头插法 (注: 这里的头插并不创建新结点)
//取原链表中的结点,头插到新结点
struct ListNode* reverseList(struct ListNode* head)
{
//当链表为空或只有一个结点并不会出现问题,无需单独判断
struct ListNode* cur = head;
struct ListNode* newHead = NULL;
while(cur)
{
struct ListNode* next = cur->next;
//头插
cur->next = newHead;
//newHead始终指向当前的头
newHead = cur;
//迭代
cur = next;
}
return newHead;
}
面试题3. 链表的中间结点
题目描述:给定一个头结点为 head
的非空单链表,返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点。
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
//快慢指针
//慢指针一次走一步,慢指针一次走两步
struct ListNode* middleNode(struct ListNode* head)
{
struct ListNode* slow = head;
struct ListNode* fast = head;
while(fast && fast->next)
{
slow = slow->next;
fast = fast->next->next;
}
return slow;
}
面试题4. 链表中倒数第k个结点
题目描述:输入一个链表,输出该链表中倒数第k个结点。
struct ListNode* FindKthToTail(struct ListNode* pListHead, int k )
{
struct ListNode* fast = pListHead, *slow = pListHead;
//fast先走k步
while(k--)
{
//要注意k比链表总长度还要大的情况,以及链表为空的情况
if(fast == NULL)
{
return NULL;
}
fast = fast->next;
}
while(fast)
{
slow = slow->next;
fast = fast->next;
}
return slow;
}
面试题5. 合并两个有序链表
题目描述:将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
//取小的结点,尾插到新链表
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2)
{
if(list1 == NULL)
return list2;
if(list2 == NULL)
return list1;
//创建新链表的头尾指针
struct ListNode* head = NULL, *tail = NULL;
//先取一个小的去做第一个结点,方便后面尾插
if(list1->val < list2->val)
{
head = tail = list1;
list1 = list1->next;
}
else
{
head = tail = list2;
list2 = list2->next;
}
while(list1 && list2)
{
//取小的尾插到新链表
if(list1->val < list2->val)
{
tail->next = list1;
list1 = list1->next;
}
else
{
tail->next = list2;
list2 = list2->next;
}
tail = tail->next; //移动尾指针以便下次插入
}
//如果有链表还没遍历一遍,直接把该链表连接到新链表的尾结点后面
if(list1)
{
tail->next = list1;
}
if(list2)
{
tail->next = list2;
}
return head;
}
//方法2: 用哨兵位的头结点简化上述代码
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2)
{
struct ListNode* head = NULL, *tail = NULL;
//创建一个哨兵位的头结点
head = tail = (struct ListNode*)malloc(sizeof(struct ListNode));
tail->next = NULL;
while(list1 && list2)
{
//取小的尾插到新链表
if(list1->val < list2->val)
{
tail->next = list1;
list1 = list1->next;
}
else
{
tail->next = list2;
list2 = list2->next;
}
tail = tail->next; //移动尾指针以便下次插入
}
//如果有链表还没遍历一遍,直接把该链表连接到新链表的尾结点后面
if(list1)
{
tail->next = list1;
}
if(list2)
{
tail->next = list2;
}
//释放申请的结点,此时需要先标记链表第一个结点的位置再释放
struct ListNode* node = head;
head = head->next;
free(node);
return head;
}
面试题6. 链表分割
题目描述:现有一链表的头指针 ListNode* pHead,给一定值x,编写一段代码将所有小于x的结点排在其余结点之前,且不能改变原来的数据顺序,返回重新排列后的链表的头指针。
ListNode* partition(ListNode* pHead, int x)
{
//开辟两个链表的头结点
ListNode* lessHead, *lessTail, *greaterHead, *greaterTail;
lessHead = lessTail =(struct ListNode*)malloc(sizeof(struct ListNode));
greaterHead = greaterTail = (struct ListNode*)malloc(sizeof(struct ListNode));
lessTail->next = NULL;
greaterTail->next = NULL;
struct ListNode* cur = pHead;
//分成两个链表
while(cur)
{
if(cur->val < x)
{
lessTail->next = cur;
lessTail = lessTail->next;
}
else
{
greaterTail->next = cur;
greaterTail = greaterTail->next;
}
cur = cur->next;
}
//链接两个链表
lessTail->next = greaterHead->next;
//关键
greaterTail->next = NULL;//很重要的一步,不然会出现死循环
pHead = lessHead->next;
free(lessHead);
free(greaterHead);
return pHead;
}
注:在链接两个链表的时候(L2链接到L1),要注意要把L2的尾指针的next指向空。因为L2是由原链表分出来的,L2的尾结点可能并不是原链表的尾结点,他的后面可能还链接着其他结点,如果next不置空可能会出现死循环。
面试题7. 链表的回文结构
题目描述:对于一个链表,请设计一个时间复杂度为 O(n) ,额外空间复杂度为 O(1) 的算法,判断其是否为回文结构(对称)。给定一个链表的头指针 A ,请返回一个bool值,代表其是否为回文结构。保证链表长度小于等于900。
bool chkPalindrome(ListNode* A)
{
//1.先找中间节点
struct ListNode* fast = A, *slow = A;
while(fast && fast->next)
{
slow = slow->next;
fast = fast->next->next;
}
struct ListNode* mid = slow;
//2.反转链表的后半部分
//此时会出现一个巧合,当只有奇数个结点时,如1,2,3,2,1
//此时mid是结点3,当反转操作结束后:1,2,1,2,3,结点2的next依然指向结点3
//***当比较操作的循环进行到 结点2的next的val与结点3的next的val比较时,其实比较的是同一个结点
struct ListNode* cur = mid;
struct ListNode* rHead = NULL;
while(cur)
{
struct ListNode* next = cur->next;
cur->next = rHead;
rHead = cur;
cur = next;
}
//3.比较
while(A && rHead)
{
if(A->val != rHead->val)
{
return false;
}
else
{
A = A->next;
rHead = rHead->next;
}
}
return true;
}
注意:在反转链表的后半部分时,会出现一个巧合,当只有奇数个结点时,如1,2,3,2,1。此时mid是结点3,当反转操作结束后链表为:1,2,1,2,3,此时(第一个)结点2的next依然指向结点3,当比较操作的循环进行到结点2的next的val与结点3的val比较时,其实比较的是同一个结点(结点3)
面试题8. 相交链表
题目描述:给你两个单链表的头节点 headA
和 headB
,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null
。 注意,函数返回结果后,链表必须 保持其原始结构 。
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
//1.判断两个链表是否相交? => 判断尾指针是否相同
//2.若相交,如何求交点?
//计算出两个链表的长度并求差值,让长的链表先走差值步。
//接着同时往后走,第一个相同的结点就是交点。(注意:是比较结点的指针而不是比较结点的val)
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB)
{
if(headA == NULL || headB == NULL)
return NULL;
struct ListNode* curA = headA, *curB = headB;
int lenA = 0, lenB = 0;
while(curA->next)
//while(curA)
{
curA = curA->next;
lenA++;
}
while(curB->next)
//while(curB)
{
curB = curB->next;
lenB++;
}
//这里要注意的是,这里算的len比实际长度少1,但由于我们需要的是差值,lenA和lenB都比实际少1,差值却不会影响,之所以要这样做是我们这样既算了长度又找到了尾结点
//同时,如果循环条件改成while(curA)也不会出问题,这样写代码就不需要判断尾结点是否相等了,先让长的链表走差值步再一起走,如果相交返回的是相交结点指针,如果不相交返回的是NULL,因为不相交的情况,循环走到最后两边都指向空指针了,不满足longList != shortList,返回的longList是NULL
//不相交的情况
if(curA != curB) //注意这里比较的是指针而不是val
return NULL;
//长的链表先走差值步,再同时走
//假设A长B短,这样写代码可读性更强
struct ListNode* longList = headA, *shortList = headB;
if(lenB > lenA)
{
longList = headB;
shortList = headA;
}
int gap = abs(lenB - lenA);
while(gap--)
{
longList = longList->next;
}
while(longList != shortList)
{
longList = longList->next;
shortList = shortList->next;
}
return longList;
}
面试题9. 环形链表
题目描述: 给你一个链表的头结点 head ,判断链表中是否有环。
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
//快慢指针fast和slow,slow每次走一步,fast走两步
//如果不带环: fast就走到尾了
//如果带环: slow和fast就会在环里相遇
bool hasCycle(struct ListNode *head)
{
struct ListNode* slow = head, *fast = head;
while(fast && fast->next)
{
slow = slow->next;
fast = fast->next->next;
if(slow == fast)
return true;
}
return false;
}
这道题本身很简单,重要的是该题在面试中的几个衍生题:
① 为什么slow走一步,fast走两步,他们就一定会在环里相遇? 会不会永远追不上呢? 请证明。
答:不会。 证明:假设slow进环的时候,fast跟slow的距离是N,进行追击的过程中,fast往前走两步,slow走一步,那么每走一步,他们之间的距离就缩小1,距离缩小到0的时候,就会相遇。
② slow走1步,fast走3步,走4步,走x步,还行不行?为什么?请证明。
答:不能保证一定会相遇。假设slow进环的时候,fast跟slow的距离是N,如果fast每次走3步,slow走1步,每走一步,他们之间的距离就缩小2,当N为偶数的时候一定会相遇,但如果N是奇数时,当N-2-2-2....一直剪到-1时,如果环的长度C是偶数(此时他们的距离C-1又是奇数),那么永远也无法相遇
总结:如果slow进环时,slow跟fast的差距N是奇数,且环的长度是偶数,那么slow和fast一直无法相遇
③ 求环的入口点
面试题10. 环形链表Ⅱ
题目描述:给定一个链表的头节点 head
,返回链表开始入环的第一个节点。 如果链表无环,则返回 null
。
注:并不存在慢指针在环里走了一圈之后,快指针才追上的情况,因为慢指针能走一圈,快指针都走了两圈了,早追上了。所以慢指针还没走到一圈的时候就一定会追上。但快指针可能会转N圈才能和慢指针相遇(L很长,环很小,那么slow指针进环时,fast已经转了N圈了)。
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode *detectCycle(struct ListNode *head)
{
struct ListNode *slow = head, *fast = head;
while(fast && fast->next)
{
slow = slow->next;
fast = fast->next->next;
//先判断是否有环
if(slow == fast)
{
//相遇点meet,起点是head
struct ListNode* meet = fast;
//通过证明得到结论:
//一个指针从head走,一个指针从meet走,最终会在入口点相遇
while(meet != head)
{
meet = meet->next;
head = head->next;
}
return head;
}
}
return NULL;
}
面试题11. 复制带随即指针的链表
题目描述:给你一个长度为 n
的链表,每个节点包含一个额外增加的随机指针 random
,该指针可以指向链表中的任何节点或空节点。
构造这个链表的 深拷贝。 深拷贝应该正好由 n 个 全新 节点组成,其中每个新节点的值都设为其对应的原节点的值。新节点的 next 指针和 random 指针也都应指向复制链表中的新节点,并使原链表和复制链表中的这些指针能够表示相同的链表状态。复制链表中的指针都不应指向原链表中的节点 。
例如,如果原链表中有 X 和 Y 两个节点,其中 X.random --> Y 。那么在复制链表中对应的两个节点 x 和 y ,同样有 x.random --> y 。
返回复制链表的头节点。
解题方法:
① 将拷贝结点链接到原结点后面(这样拷贝结点的random等于原结点random的next)
② 处理拷贝结点的random
③ 把拷贝结点截下来,链接到一起
/**
* Definition for a Node.
* struct Node {
* int val;
* struct Node *next;
* struct Node *random;
* };
*/
//方法1:拷贝以后,新链表需要确定每一个random,需要去自己的链表中去查找,是O(N),每个random都要处理就是O(N*N),效率就低了。且,如果链表中存在结点的值相同,那么就更复杂了(比如有多个结点的值是7,我们只知道random指向7但不知道是第几个7)。
//方法2:
//1.将拷贝结点链接到原结点后面(这样拷贝结点的random等于原结点random的next)
//2.处理拷贝结点的random
//3.把拷贝结点截下来,链接到一起,恢复原链表
struct Node* copyRandomList(struct Node* head)
{
//处理空链表
if(head == NULL)
return NULL;
//1.拷贝结点挂在原结点后面,建立对应关系
struct Node* cur= head;
while(cur)
{
//插入拷贝结点
struct Node* copy = (struct Node*)malloc(sizeof(struct Node));
struct Node* next = cur->next;
copy->val = cur->val;
cur->next = copy;
copy->next = next;
//迭代
cur = next;
}
//2.处理copy结点的random
cur = head;//重新指回头结点
while(cur)
{
struct Node* copy = cur->next; //注意此时的copy不是malloc出来的
if(cur->random == NULL)//如果原结点random为NULL, 拷贝结点random也为NULL
{
copy->random = NULL;
}
else
{
//拷贝结点的random等于原结点random的next
copy->random = cur->random->next;
}
//迭代
cur = copy->next;
}
//3.拷贝结点取下来链接(尾插)到一起、恢复原链表
cur = head;
//尾插用带头结点的链表更方便(第一个结点不用单独操作)
struct Node* copyHead, *copyTail;
copyHead = copyTail = (struct Node*)malloc(sizeof(struct Node));
while(cur)
{
struct Node* copy = cur->next;
struct Node* next = copy->next;
//尾插
copyTail->next = copy;
copyTail = copy;//移动尾指针
//恢复原链表
cur->next = next;
//迭代
cur = next;
}
//释放申请空间
struct Node* guard = copyHead;
copyHead = copyHead->next;
free(guard);
return copyHead;
}
面试题12. 对链表进行插入排序
题目描述:给定单个链表的头 head
,使用 插入排序 对链表进行排序,并返回 排序后链表的头 。
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode* insertionSortList(struct ListNode* head)
{
if(head==NULL || head->next == NULL)
return head;
//初始条件:
//把第一个结点拿出来
struct ListNode* sortHead = head;
struct ListNode* cur = head->next; //该步骤要在将sortHead->next置空前, 不然就找不到了
sortHead->next = NULL;//不置空可能会出现死循环
//中止条件,最后一个结点插入进去就结束了
while(cur)
{
//3.迭代条件
struct ListNode* next = cur->next;//next用于cur插入后,迭代时找到原cur的下一个结点
//将cur结点插入到前面的有序区间
struct ListNode* p = NULL, *c = sortHead; //c表示有序链表中的当前位置,p表示c的前一个结点(方便在c之前插入)
while(c)
//注意*:当c==NULL循环结束,此时可能会有两种情况,1.cur的val比c的val小;2.cur比有序链表中的所有数都要大(p和c一直往前,c走到NULL时结束循环),这两个情况都需要插入,所以把插入操作放到了该循环外面
{
//如果cur比c小 => 那么直接在c之前插入,如果cur比c大 => p和c都往后走一步,c再与cur比较
if(cur->val < c->val)
{
break;
}
else
{
//如果cur比c大 => p和c都往后走一步
p = c;
c = c->next;
}
}
//插入操作
//如果cur比第一个c小,那么需要在第一个结点之前(p->next)进行插入(头插),而p为NULL,所以需要单独处理
if(p == NULL)
{
cur->next = c;
sortHead = cur;
}
else
{
//中间部分的插入
p->next = cur;
cur->next = c;
}
//此时一次插入操作完成了,cur指向下一个数字
cur = next;
}
return sortHead;
}
面试题13. 删除链表中的重复结点
题目描述: 在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留,返回链表头指针。 例如,链表 1->2->3->3->4->4->5 处理后为 1->2->5,要求:空间复杂度 O(n) ,时间复杂度 O(n)
struct ListNode* deleteDuplication(struct ListNode* pHead )
{
if(pHead == NULL || pHead->next == NULL)
return pHead;
//起始条件
struct ListNode *prev = NULL, *cur = pHead, *next = cur->next;
while(next)
{
if(cur->val == next->val)
{
//删除重复的结点
//next往后走,找到和cur值不同的结点停下
//*注意这里要先判断next是否为NULL,当next为空,再执行next->val就会出错
while(next && cur->val == next->val)
{
next = next->next;
}
//删掉cur到next之间的结点
while(cur != next)
{
//记录被删掉的结点,让cur向后走之后释放掉该空间
struct ListNode* del = cur;
cur = cur->next;
free(del);
}//循环结束时cur = next
//链接。此时分为两种情况
//1.当prev为空,移动链表的头结点到cur
if(prev == NULL)
{
pHead = cur;
}
//当prev不为空,直接让prev的next等于cur
else
{
prev->next = cur;
}
//循环结束时cur = next,之后要让next指向下一个结点,需要判断此时next是否为空
if(next)
next = next->next;
}
else
{
//迭代
prev = cur;
cur = next;
next = next->next;
}
}
return pHead;
}
对于复杂问题,画图分析对于解题是非常重要的!!!
4. 带头双循环链表
4.1 接口实现
List.h
#pragma once
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
typedef int LTDataType;
typedef struct ListNode
{
struct ListNode* next;
struct ListNode* prev;
LTDataType data;
}ListNode;
//创建一个结点
ListNode* BuyListNode(LTDataType x);
//初始化
ListNode* ListInit();
//尾插
void ListPushBack(ListNode* phead, LTDataType x);
//头插
void ListPushFront(ListNode* phead, LTDataType x);
//尾删
void ListPopBack(ListNode* phead);
//头删
void ListPopFront(ListNode* phead);
//查找
ListNode* ListFind(ListNode* phead, LTDataType x);
//在pos指针位置(前面)插入
void ListInsert(ListNode* pos, LTDataType x);
//在pos指针位置删除
void ListErase(ListNode* pos);
//判空(空返回1,非空返回0)
int ListEmpty(ListNode* phead);
//链表结点个数
int ListSize(ListNode* phead);
//销毁链表
void ListDestory(ListNode* phead);
//打印
void ListPrint(ListNode* phead);
List.c
#include "List.h"
//创建一个结点
ListNode* BuyListNode(LTDataType x)
{
ListNode* node = (ListNode*)malloc(sizeof(ListNode));
node->next = NULL;
node->prev = NULL;
node->data = x;
return node;
}
//初始化
//由于是双向带头循环链表,所以此时的next和prev都指向他们自己
ListNode* ListInit()
{
ListNode* phead = BuyListNode(0);
//这样初始化可以让插入操作不需要单独考虑只有phead的情况
phead->next = phead;
phead->prev = phead;
return phead;
}
//尾插
void ListPushBack(ListNode* phead, LTDataType x)
{
//assert(phead);
找尾
//ListNode* tail = phead->prev;
//ListNode* newnode = BuyListNode(x);
尾插
//tail->next = newnode;
//newnode->prev = tail;
//newnode->next = phead;
//phead->prev = newnode;
如果插入第一个结点需不需要修改上述代码? 并不
这就是双向带头循环链表的优势:结构虽然复杂但操作反而简单
//代码复用
//在phead前面插入,相当于尾删
ListInsert(phead, x);
}
//头插
//同样不用考虑只有phead的情况
void ListPushFront(ListNode* phead, LTDataType x)
{
//assert(phead);
用first记录下第一个结点后,头插时就不需要考虑插入的步骤
//ListNode* first = phead->next;
//ListNode* newnode = BuyListNode(x);
//
头插 phead newnode first
//phead->next = newnode;
//newnode->prev = phead;
//newnode->next = first;
//first->prev = newnode;
//代码复用
//相当于在head->next之前插入
ListInsert(phead->next, x);
}
//尾删
//不用考虑phead后只有一个结点的情况,但要考虑使用该函数把链表删除到只剩phead时,再调用该函数就会删除phead
void ListPopBack(ListNode* phead)
{
//assert(phead);
//assert(phead->next != phead);//只剩phead的情况
找到尾结点及其前驱
//ListNode* tail = phead->prev;
//ListNode* tailPrev = tail->prev;
尾删
//free(tail);
//tailPrev->next = phead;
//phead->prev = tailPrev;
//复用
ListErase(phead->prev);
}
//头删
//也要考虑删除到只有phead的情况
void ListPopFront(ListNode* phead)
{
/*assert(phead);
assert(phead->next != phead);
ListNode* first = phead->next;
ListNode* second = first->next;
free(first);
phead->next = second;
second->prev = phead;*/
//复用
ListErase(phead->next);
}
//查找
ListNode* ListFind(ListNode* phead, LTDataType x)
{
assert(phead);
ListNode* cur = phead->next;
while (cur != phead)
{
if (cur->data == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
//在pos指针位置插入
void ListInsert(ListNode* pos, LTDataType x)
{
assert(pos);
ListNode* prev = pos->prev;
ListNode* newnode = BuyListNode(x);
prev->next = newnode;
newnode->prev = prev;
newnode->next = pos;
pos->prev = newnode;
}
//在pos指针位置删除
void ListErase(ListNode* pos)
{
assert(pos);
//找到pos的前驱后驱
ListNode* prev = pos->prev;
ListNode* next = pos->next;
prev->next = next;
next->prev = prev;
free(pos);
//当pos是phead->prev的时候就是尾删
//pos是phead->next就是头删
}
//判空(空返回1,非空返回0)
int ListEmpty(ListNode* phead)
{
assert(phead);
return phead->next == phead ? 1 : 0;
}
//链表结点个数
int ListSize(ListNode* phead)
{
assert(phead);
int size = 0;
ListNode* cur = phead->next;
while (cur != phead)
{
size++;
cur = cur->next;
}
return size;
}
//销毁链表
//*注调用后要把phead置空
void ListDestory(ListNode* phead)
{
assert(phead);
//从第一个结点开始销毁
ListNode* cur = phead->next;
while (cur != phead)
{
ListNode* next = cur->next;//提前保存cur的next,免得释放cur后找不到了
free(cur);
cur = next;
}
//最后把phead销毁,*注意*想销毁phead有两个办法:1.传入二级指针2.用完该函数之后加一句plist = NULL
free(phead);
//phead = NULL;//不起作用,此处选择不用二级指针是为了保证接口的一致性
}
//打印
void ListPrint(ListNode* phead)
{
//cur指向头结点的下一个结点
ListNode* cur = phead->next;
while (cur != phead)
{
printf("%d ", cur->data);
cur = cur->next;
}
printf("\n");
}
test.c
#include "List.h"
//测试: 头插、尾插、头删、尾删
void TestList1()
{
/*ListNode* plist;
ListInit(&plist);*/
ListNode* plist = ListInit();
//不需要传二级指针,因为plist始终指向头结点的
ListPushBack(plist, 1);
ListPushBack(plist, 2);
ListPushBack(plist, 3);
ListPushBack(plist, 4);
//ListPrint(plist); //1 2 3 4
ListPushFront(plist, 0);
//ListPrint(plist); //0 1 2 3 4
ListPopBack(plist);
//ListPrint(plist); //0 1 2 3
ListPopFront(plist);
//ListPrint(plist); //1 2 3
}
void TestList2()
{
ListNode* plist = ListInit();
ListPushBack(plist, 1);
ListPushBack(plist, 2);
ListPushBack(plist, 3);
ListPushBack(plist, 4);
ListPushBack(plist, 5);
ListPushBack(plist, 6);
ListNode* pos = ListFind(plist, 4);
ListInsert(pos, 40);//判断pos是否为空在该函数内部
//ListPrint(plist); //1 2 3 40 4 5 6
}
int main()
{
TestList1();
return 0;
}
优点:可以该链表的任意位置插入删除(不能删除head),无需考虑特殊情况单独判断。
面试题:给你15min实现一个带头双循环链表
方法:直接先实现在pos位置插入、删除,这样头删尾删头插尾插直接复用即可
5. 顺序表与链表的优缺点对比
注:此处的链表是指双向带头循环链表
5.1 顺序表的优缺点
优点:
① 空间连续,可以按下标进行随机访问
② 顺序表的cpu高速缓存命中率比较高
缺点:
① 空间不够需要增容,增容有一定的性能消耗,且可能存在一定的空间浪费
② 头部或者中间插入删除数据,需要挪动数据,效率比较低 -> O(N)
5.2 链表的优缺点
优点:
① 按需申请内存,需要存一个数据就申请一块内存,不存在空间浪费。
② 任意位置O(1)时间插入删除数据
缺点:
① 不支持下标的随机访问
总结:这两个数据结构是相辅相成的,他们互相弥补对方的缺点,需要用谁存数据,具体得看场景