一、时间复杂度和空间复杂度
1、什么是算法效率?
- 算法效率分为时间效率和空间效率,时间效率被称为时间复杂度而空间效率称为空间复杂度;
- 时间复杂度是主要衡量一个算法的运行速度,而空间复杂度主要衡量的是算法所需要的额外空间;
2、什么是时间复杂度?
时间复杂度是主要衡量一个算法的运行速度,因不能每次都让算法上机测试计算时间复杂度,所以将算法中的基本操作的执行次数,作为算法的时间复杂度;
- 如何计算时间复杂度?
答:大概计算算法的执行次数,用大O的渐进表示法进行表示;
确定大O阶方法:
(1)用常数1取代运行时间中的所有加法常数;
(2)遍历的运行次数,只保留最高阶项;
(3)若最高阶存在且不是1,则可以忽略与其相乘的常数;- 实际上一般关注的是算法的最坏运行情况;
例:
二分查找的时间复杂度为:o(logN)
int BinarySearch(int* a, int n, int x)
{
//二分查找
assert(a);
int begin = 0;
int end = n-1;
while (begin < end)
{
int mid = begin + ((end-begin)>>1);
if (a[mid] < x)
begin = mid+1;
else if (a[mid] > x)
end = mid;
else
return mid;
}
return -1;
}
递归斐波那契时间复杂度为O(2^N)
(2+……+2^N)
long long Fibonacci(size_t N)
{
return N < 2 ? N : Fibonacci(N-1)+Fibonacci(N-2);
}
目前因为计算机的不断发展,其存储容量有了一定的提升,所以现在主要关注的是一个算法的时间复杂度;
3、空间复杂度的概念?
计算一个算法在运行过程中临时占用存储空间大小的度量;(即计算的是变量的个数,同样利用大O渐进表示法)
例:
(递归调用了N次,开辟了N个栈帧,每个栈帧用了常数个空间所以空间复杂度为O(N))
long long Factorial(size_t N)
{
return N < 2 ? N : Factorial(N-1)*N;
}
二、链表和顺序表
1、线性表(顺序表、链表、栈、队列等都是线性表)
线性表是n个具有相同特性的数据元素的有限序列,是一种常见的数据结构;
-线性表逻辑上表现为连续的一条线进行存储,但是物理结构上并不一定是连续的,通常物理存储上是以数组和链式形式存储的;
2、顺序表
(1)什么是顺序表?
顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下用数组出现,利用数组完成数据增删改查;
顺序表可以分为:
a)静态顺序表:使用定长数组存储(一般用于确定知道要存储多少数据的场景)
b)动态顺序表:使用动态开辟数组存储(常用)
(2)实现动态顺序表
(动态顺序表基本结构)
typedef struct SeqList
{
SLDataType* array; // 指向动态开辟的数组
size_t size ; // 有效数据个数
size_t capicity ; // 容量空间的大小(可增容)
}SeqList;
冒泡排序和二分查找
//冒泡排序
void SeqListBubbleSort(SeqList* psl)
{
assert(psl);
int i, j;
SLDataType tmp;
for (i = 0; i < psl->size - 1; i++)
{
for (j = 0; j < psl->size - 1 - i; j++)
{
if (psl->array[j] > psl->array[j+1])
{
tmp = psl->array[j];
psl->array[j] = psl->array[j + 1];
psl->array[j + 1] = tmp;
}
}
}
}
//二分查找
int SeqListBinaryFind(SeqList* psl, SLDataType x)
{
assert(psl);
int left = 0;
int right = psl->size - 1;//for(int i = 0;;)尽量不要这么写,一般情况下
//将定义的变量直接写在最上面即可。
int mid;
while (left <= right)
{
mid = (left + right) / 2;
if (psl->array[mid] < x)
{
left = mid + 1;
}
else if (psl->array[mid] > x)
{
right = mid - 1;
}
else
{
return mid;
}
}
return -1;
}
具体代码:https://github.com/Wilingpz/sunny/tree/master/6.22%E9%A1%BA%E5%BA%8F%E8%A1%A8
(3)顺序表存在问题:
1)在中间和头部插入的时间复杂度为O(N);
2)增容需要申请空间,拷贝数据,释放就空间,消耗较大;
3)增容一般成2倍增长,会有一定的空间浪费;
3、链表
1、链表的概念
链表是一种物理存储结构上非连续,非顺序的存储结构,其逻辑顺序是根据链表中的指针链接次序实现的;
2、链表的类型
单向、双向;
带头、不带头;
循环、非循环;(组合起来就有8种)
常用的有两种:无头单向非循环链表和带头双向循环链表;
3、简述两种常用链表类型
(1)无头单向非循环链表
结构简单,一般不会单独用来存储数据,多用于作为其他数据结构的子结构,如哈希桶等;
(2)带头双向循环链表
结构最复杂,一般用于单独存储数据,很常用;
4、链表的实现
1)无头单向非循环链表
//在值确定的某个结点前进行插入
void SListInsertFront(SList* plist, SLTDataType x, SLTDataType src)
{
assert(plist);
SListNode * cur;
SListNode * newdata = (SListNode *)malloc(sizeof(SListNode));
if (plist->_head->_data == x)//如果该结点是头结点,直接进行头插
{
SListPushFront(plist, src);
return;
}
for (cur = plist->_head; cur->_next; cur = cur->_next)//找到这个结点
{
if (cur->_next->_data == x)//注意这里查的是cur的下一个结点等于x
{
break;
}
}
newdata->_data = src;//进行插入
newdata->_next = cur->_next;
cur->_next = newdata;
}
//在某结点后进行插入
void SListInsertAfter(SListNode* pos, SLTDataType x)
{
assert(pos);
SListNode * cur = (SListNode *)malloc(sizeof(SListNode));
cur->_data = x;
cur->_next = pos->_next;
pos->_next = cur;
}
具体代码:https://github.com/Wilingpz/sunny/tree/master/6.22%E9%93%BE%E8%A1%A8
2)带头双向循环链表
头插和头删
void ListPushFront(List* plist, LTDataType x)
{
ListNode *cur = (ListNode *)malloc(sizeof(ListNode));
cur->_data = x;
plist->_head->_next->_prev = cur;//建立cur和头结点的下一个结点的联系
cur->_next = plist->_head->_next;
plist->_head->_next = cur;//建立head和cur的联系
cur->_prev = plist->_head;
}
//头删
void ListPopFront(List* plist)
{
ListNode *tmp = plist->_head->_next;
if (tmp != plist->_head)
{
tmp->_prev->_next = tmp->_next;
tmp->_next->_prev = tmp->_prev;
free(tmp);
}
}
三、顺序表(数组)与链表的区别与联系
顺序表:空间连续、支持随机访问但是中间和头部插入时间复杂度为O(N),且增容代价太大;
链表:以结点为单位存储,任意位置插入的时间复杂度为O(1),不存在增容问题,但是不支持随机访问。
四、关于链表的一些问题
1、链表如何判断是否有环?如果有环,如何确定入口点?
(1)设置两个指针同时指向head,其中一个一次前进一个节点(P1),另外一个一次前进两个节点(P2)。 p1和p2同时走,如果其中一个遇到null,则说明没有环,如果走了N步之后,二者指向地址相同,那么说明链表存在环。(一个速度为另一个速度的二倍,所以如果有环的话速度快的会在某个地方追上速度慢的)
代码:
#include <stdio.h>
typedef struct Node
{
int val;
Node *next;
}Node,*pNode;
//判断是否有环
bool isLoop(pNode pHead)
{
pNode fast = pHead;
pNode slow = pHead;
//如果无环,则fast先走到终点
//当链表长度为奇数时,fast->Next为空
//当链表长度为偶数时,fast为空
while( fast != NULL && fast->next != NULL)
{
fast = fast->next->next;
slow = slow->next;
//如果有环,则fast会超过slow一圈
if(fast == slow)
{
break;
}
}
if(fast == NULL || fast->next == NULL )
return false;
else
return true;
}
//计算环的长度
int loopLength(pNode pHead)
{
if(isLoop(pHead) == false)
return 0;
pNode fast = pHead;
pNode slow = pHead;
int length = 0;
bool begin = false;
bool agian = false;
while( fast != NULL && fast->next != NULL)
{
fast = fast->next->next;
slow = slow->next;
//超两圈后停止计数,挑出循环
if(fast == slow && agian == true)
break;
//超一圈后开始计数
if(fast == slow && agian == false)
{
begin = true;
agian = true;
}
//计数
if(begin == true)
++length;
}
return length;
}
(2)确定环的入口点在哪里?
【如果单链表有环,当slow指针和fast指针相遇时,slow指针还没有遍历完链表,而fast指针已经在环内循环n(n>=1)圈了】
其中:
假设相遇时slow指针走了s步;
fast指针走了2s步;
r为fast在环内转了一圈的步数;
a为从链表头到入口点的步数;
b为从入口点到相遇点的步数;
c为从相遇点再走c步到达入口点;
L为整个链表的长度;
即就是链表头到环入口点等于(n - 1)循环内环 + 相遇点到环入口点(从头结点到入口的距离,等于转了(n-1)圈以后,相遇点到入口的距离)
- 故在链表头和碰撞点点分别设置一个指针,同时出发,每次各走一步,它们必定会相遇,且第一次相遇的点就是环入口点。
Node* findLoopEntrance(pNode pHead)
{
pNode fast = pHead;
pNode slow = pHead;
while( fast != NULL && fast->next != NULL)
{
fast = fast->next->next;
slow = slow->next;
//如果有环,则fast会超过slow一圈
if(fast == slow)
{
break;
}
}
if(fast == NULL || fast->next == NULL)
return NULL;//表示没有环
slow = pHead;//fast指向相遇点,slow指向起始点
while(slow != fast)
{
slow = slow->next;
fast = fast->next;
}
//两个指针相遇的时候就是环的入口结点处
return slow;
}
2、链表如何判断是否相交?如果相交找到交点。
(1)最简单直接的方法;
遍历两个链表,判断第一个链表的每个结点是否在第二个链表中,时间复杂度为O(len1*len2),耗时很大;
顺序查询到第一个在第二个链表种的节点即是两个链表的交点。
(2)使用栈
1)从头遍历两个链表;
2)创建两个栈,第一个栈存储第一个链表的节点,第二个栈存储第二个链表的节点,每遍历到一个节点时,就将该节点入栈;
3)两个链表都入栈结束后,则通过top判断栈顶的节点是否相等即可判断两个单链表是否相交。因为我们知道,若两个链表相交,则从第一个相交节点开始,后面的节点都相交;
3)若两链表相交,则循环出栈,直到遇到两个出栈的节点不相同,则这个节点的前一个节点就是第一个相交的节点。
node temp=NULL; //存第一个相交节点
while(!stack1.empty()&&!stack2.empty())
//两栈不为空
{
temp=stack1.top();
stack1.pop();
stack2.pop();
if(stack1.top()!=stack2.top())
{
break;
}
}
(3)遍历链表记录长度
1)同时遍历两个链表到尾部,同时记录两个链表的长度。若两个链表最后的一个节点相同,则两个链表相交。
2)有两个链表的长度后,我们就可以知道哪个链表长,设较长的链表长度为len1,短的链表长度为len2。
3)则先让较长的链表向后移动(len1-len2)个长度。然后开始从当前位置同时遍历两个链表,当遍历到的链表的节点相同时,则这个节点就是第一个相交的节点。
(4)哈希表法
建立两个哈希表,将两个链表的结点和地址添加进去, 判断两个链表中是否存在地址一致的节点,就可以知道是否相交了,同样也能找到第一个相交结点;
(5)问题转化为判断一个链表是否有环问题
先判断两个链表是否有环?(因为两个单链表不可能交叉相交,因为每个结点就只有一个指针域)
先遍历第一个链表到它的尾部,然后将尾部的next指针指向第二个链表(尾部指针的next本来指向的是null)。这样两个链表就合成了一个链表。若该链表有环,则原两个链表一定相交。否则,不相交。
具体方法同1;
3、有序链表合并?
(1)若要求不能对原始链表更改,则必须使用额外空间;
(遍历比较大小,将较小的插入到新链表里,若两个链表不一样长,最后将长的多余部分添加到链表尾部即可)
(2)更改原始链表 主要利用循环实现;
(3)一般绝大多数链表和树的题目都可以用递归实现,注意递归出口条件;
代码:
(1)开辟新结点
//使用额外空间来合并链表 不对原始链表做改变
node* mergeTwoLinkListWithExtraPlace(node *head1, node *head2) {
/*先创建一个头结点 这里用任意的整数都可以,不一定用0,之后返回newHead->next即可*/
node *newHead = new node(0);
node *tail = newHead; //记录尾节点 方便尾插法
node *p = head1;
node *q = head2;
while (p && q) //p和q都不为空的情况下一一进行比较
{
if (p->data < q->data)
{
node *r = new node(p->data);//申请一个新结点
p = p->next;
tail->next = r;
}
else
{
node *r = new node(q->data);
q = q->next;
tail->next = r;
}
tail = tail->next;
}
//如果有一个链表到达了尾部,将那个没有到尾部的剩余部分添加到新链表尾部
while (p)
{
node *r = new node(p->data);
tail->next = r;
tail = tail->next;
p = p->next;
}
while (q)
{
node *r = new node(q->data);
tail->next = r;
tail = tail->next;
q = q->next;
}
return newHead->next;
//这里头结点直接丢弃 保证完整性 可以释放头结点 如下
/*node *r = newHead;
newHead = newHead->next;
delete r;
r = nullptr;
return newHead;*/
}
代码(2):直接在原链表上进行更改
//非递归的方式 也成迭代法
node* mergeTwoSortedLinkListWithoutRecursion(node* head1, node* head2)
{
node* newHead = new node(0); //先创建一个链表头结点 返回的时候返回头结点下一个结点即可
node *p = head1;
node *q = head2;
node* tail = newHead; //这个结点用来记录合并后链表的尾节点 方便进行尾插法
while (p && q)
{
if (p->data < q->data)
{
tail->next = p;
p = p->next;
}
else
{
tail->next = q;
q = q->next;
}
tail = tail->next;
}
//以下情况是有一个链表走到了尾部
if (p)
{
tail->next = p;
}
if (q)
{
tail->next = q;
}
return newHead->next; //这里头结点最好释放一下 如下
/*node *r = newHead;
newHead = newHead->next;
delete r;
r = nullptr;
return newHead;*/
}
代码(3):递归
node* mergeTwoSortedLinkListWithRecursion(node* head1, node* head2)
{
//如果head1 和 head2有一个为空 则直接返回另一个
if (!head1)
{
return head2;
}
if (!head2)
{
return head1;
}
//递归可以理解为之后的情况都处理好了 只需要解决好当前这步就行了
if (head1->data < head2->data)
{
head1->next = mergeTwoSortedLinkListWithRecursion(head1->next, head2);
return head1;
}
else {
head2->next = mergeTwoSortedLinkListWithRecursion(head1, head2->next);
return head2;
}
}