简单复习一下数据结构。
数据结构
数据结构是数据元素与元素之间关系的集合,根据数据元素之间关系的基本特性,有四种基本的数据结构:
集合: 数据元素间除"同属于一个集合"外,无其它关系(元素与元素之间是没有关系的)
线性结构: 元素与元素之间具有一对一关系(所有的元素具有前后依存关系,除了第一个元素以外其他所有的元素都有且仅有一个前驱,除了最后一个元素以外所有的元素都有且仅有一个后继);数组、栈、队列、链表
树: 元素与元素之间是一对多的关系(除了第一个元素以外所有的元素都只有一个唯一的前驱,但是元素的后继的个数是任意多个);树,二叉树
图: 元素与元素之间是多对多的关系。图
数据结构包括逻辑结构和存储结构:
逻辑结构: 抽象反映数据元素的逻辑关系; 一般情况下数据的逻辑结构包括线性结构与非线性结构
存储结构: 物理结构,数据的逻辑结构在计算机存储器中的实现,一般分为顺序结构(用元素在内存中的位置来表示元素与元素之间的关系)和链式结构(用元素存储位置的指针来表示元素之间的逻辑关系)
算法分析:
算法: 解决问题的方法和步骤;指令的有限的有序序列。
算法分析: 对算法的时间或者空间使用的效率进行评估与分析,一般使用时间复杂度和空间复杂度来描述。
时间分析:
事后统计: time ./test
时间复杂度: 算法中核心关键代码的执行次数和问题规模的函数,当问题规模趋近于无穷大的时候,该函数无限接近于某个规律函数,就用这个规律函数来表示时间复杂度,使用O表示
int sum = 0;
int arr[100] = {0};
for(int i = 0; i < 100; i++)
{
arr[i] = rand() % 100;
sum += arr[i];
}
cout << sum << endl;
核心关键代码有两条(循环体)
ARM指令系统的特点ARM是精简指令集系统(RISC精简指令集系统, CISC复杂指令集系统),每一条指令的执行时间是相等的;如果两条关键代码对应的汇编指令(机器指令)可能是4条,那么则4条指令的执行时间是相等的;所以核心关键执行次数和问题规模n的关系函数是:
f(n) = 4n;
这个函数当n趋向于无穷大的时候和关键函数y = n的变化趋势是完全一样的,所以这个算法的时间复杂度就是: O(n)
for(i = 0; i < 100; i++)
{
for(j = i + 1; j < 100; j++)
{
if(arr[i] < arr[j])
{
arr[i] += arr[j];
arr[j] = arr[i] - arr[j];
arr[i] -= arr[j];
}
}
}
核心关键代码的执行次数是: 1 + 2 + 3 + 4 + ... + (n-1)
指令的执行次数: 3 * n * (n - 1) / 2
核心关键代码和问题规模的函数: f(n) = n * (n - 1) / 2
当n趋向无穷大的时候和n^2的变化趋势相同,所以时间复杂度为: O(n^2)
常见的时间复杂度: 常数时间O(1) --> 对数时间O(logn) --> 线性时间O(n) --> 线性对数时间O(nlogn) --> 次方阶O(n^2) --> O(n ^ 3) ... --> 指数阶O(2^n)
空间复杂度: 一般使用算法实现过程中使用的辅助空间和问题规模的函数当问题规模趋近于无穷大的时候的趋势函数,也使用O表示,一般常见的空间复杂度为: O(1)和O(n)
线性表
一个线性表是n个数据元素的有限序列。
特征:
元素个数n—表长度,n=0空表
1<i<n时
ai的直接前驱是ai-1,a1(第一个元素)无直接前驱
ai的直接后继是ai+1,an(最后一个元素)无直接后继
元素同构(所有的元素是相同类型的),且不能出现缺项(在逻辑上是连续的)
线性表的实现方式或者存储方式有两种: 顺序存储 -- 顺序表,链式存储 -- 链表
顺序表: 采用数组来表示,用一组地址连续的存储单元存放一个线性表
特点:
实现逻辑上相邻—物理地址相邻(所有的元素在内存中依次连续存储)
实现随机存取(可以通过序号访问数组的元素)
缺点: 在顺序表中插入或者删除元素的效率比较低(需要移动大量元素)
//查找插入位置
for(i = 0; i < n; i++)
{
if(arr[i] > num)
{
pos = i;
break;
}
}
if(i > n - 1)
{
pos = n ;
}
//移动
for(i = n - 1; i >= pos; i--)
{
arr[i + 1] = arr[i];
}
arr[pos] = num;
链式表: 使用链表表示
特点:
用一组任意的存储单元存储线性表的数据元素(逻辑上相邻的元素在物理上可以不相邻)
利用指针实现了用不相邻的存储单元存放逻辑上相邻的元素(只支持顺序访问)
每个数据元素ai,除存储本身信息外,还需存储其直接后继的信息(需要额外的存储空间)
插入或者删除元素可以实现O(1) -- 插入算法的时间复杂度是O(n),插入操作的时间复杂度是O(1)
单链表特点
它是一种动态结构,整个存储空间为多个链表共用
不需预先分配空间
指针占用额外存储空间
不能随机存取,查找速度慢
struct Node
{
ElemType Data;
struct Node *next;
};
表示链表:
a. 头指针法: 使用一个指针指向链表的第一个节点
b. 头节点法: 第一个节点仅用于表示链表的开始,其数据域不具有实际含义(可以用来存储一些辅助信息)
不管是头指针还是头节点表示链表,在操作的时候通常都是使用一个指针指向链表的开始(第一个节点)
链表为空的判定:
头指针法: head == NULL
头结点: head->next == NULL;
按照节点中定义的指针域的不同可以分为: 单链表(节点中有一个指针域next,该指针指向链表的下一个节点);双向链表(节点中有两个指针域pre和next,分别指向链表中该节点的前一个节点(pre)和后一个节点(next));
在单链表和双向链表中正向最后一个节点的next为NULL,反向(如果是双向链表的话)的最后一个节点(实际上就是第一个节点)的pre为NULL
如果单链表的最后一个节点的指针域next不是为NULL而是指向第一个节点(tail->next = head), 则该单向链表称为循环链表
如果双向链表的最后一个节点的next指向第一个节点,第一个节点的pre指向最后一个节点,则称为双向循环链表
链表的操作
创建链表:
头插法: 在链表头插入节点
p->next = head;
head = p;
头结点法:
p->next = head->next;
head->next = p;
尾插法: 在链表的最后一个节点的后面插入一个节点
tail->next = p;
在操作的时候tail从head开始,一直遍历到tail->next为NULL为止
tail = head;
while(tail->next)
{
tail = tail->next;
}
插入节点:
a. 找到插入位置,假如p指向要插入的位置
使用两个指针,最开始的时候都为NULL,然后同时往后移动,一个指针指向当前节点,一个指针指向当前节点的前一个节点,
b. 插入
p指向要插入的位置,q指向插入位置的前一个节点,插入node
(在q和p之间插入node)
node->next = p; //或者node->next = q->next;
q->next = node;
双向链表在p指向的位置插入node
node->next = p;
p->pre->next = node;
node->pre = p->pre;
p->pre = node;
双向链表中删除p指向的节点
p->pre->next = p->next;
p->next->pre = p->pre;
p->pre = p->next = NULL;
free(p);
//link.h -- 头文件
#ifndef _LINK_HEAD_
#define _LINK_HEAD_
typedef int ElemType; //元素的类型
#define int bool //在C语言中使用逻辑类型,在C99以后可以使用_Bool来定义逻辑类型
#define true 1
#define false 0
//节点类型
typedef struct node
{
ElemType data;
struct node *next;
}Node;
//链表类型
typedef Node *Link;
//接口定义
Link newNode(int data);
void insertHead(Link *head, ElemType data); //头插法
void insertTail(Link *head, ElemType data); //尾插法
void insertSortAsc(Link *head, ElemType data); //排序
void deleteNode(Link *head, ElemType data); //删除节点
Link findMidNode(Link L); //查找中间节点
_Bool isLoop(Link L); //判定链表是否有环
void reverseEvenNode(Link *L); //偶数节点翻转
void show(Link L);
#endif
//link.c
#include "link.h"
#include <stdio.h>
#include <stdlib.h>
//创建节点: 采用动态内存分配的方式来创建一个节点,data节点的数据域的值
Link newNode(ElemType data)
{
//动态建立一个节点
Link node = (Link)malloc(sizeof(Node));
node->data = data;
node->next = NULL;
return node; //返回在函数中动态分配的空间 -- 有风险
}
//创建链表--头插法
void insertHead(Link *head, ElemType data)
{
//在函数中动态分配的空间需要在函数外部释放的时候
//*head = (Link)malloc(sizeof(Node));
Link node = newNode(data);
if(NULL == node) //如果分配空间失败
{
return;
}
//如果原来的链表为空
if(NULL == *head)
{
*head = node;
}
else
{
node->next = *head;
*head = node;
}
}
//尾插法: 首先通过遍历找到链表的尾部(最后一个节点),然后修改该节点的next
void insertTail(Link *head, ElemType data)
{
Link node = newNode(data);
//遍历链表: 定义一个指针来从链表头开始进行遍历
Link xhead = *head;
//如果原来链表为空
if(NULL == xhead)
{
*head = node;
return;
}
//遍历链表,找到最后一个节点
while(NULL != xhead->next)
{
xhead = xhead->next;
}
//将新节点插入到链表末尾
xhead->next = node;
}
//随机插入: 在一个有序链表中插入一个节点保持链表有序
void insertSortAsc(Link *head, ElemType data)
{
Link node = newNode(data);
Link xhead = *head; //执行当前节点
Link pre = NULL; //指向当前节点的前一节点
if(NULL == xhead)
{
*head = node;
return;
}
//使用两个指针进行遍历(两个指针一个指向当前节点,一个指向当前节点的前一个节点,并且同时移动)
while(NULL != xhead && data > xhead->data)
{
pre = xhead;
xhead = xhead->next;
}
//插入: 有三种可能 -- 要插入的是最小的节点值, 要插入的是最大的节点值, 要插入的值是在链表中间
//a. 要插入的是最小的 -- 插入在链表的头部
if(NULL == pre)
{
node->next = *head;
*head = node;
return;
}
//b. 要插入的是在中间位置--在pre和xhead中间插入
//c. 要插入的是在链表的尾部 -- xhead = NULL,pre指向链表的最后一个节点,所以还是在pre和xhead之间插入
else
{
node->next = xhead;
pre->next = node;
}
}
//删除节点
void deleteNode(Link *head, ElemType data)
{
Link pre = NULL;
Link cur = *head;
//遍历链表,找到要删除的节点
while(NULL != cur)
{
if(cur->data == data) //当前的cur指向的节点是要删除的节点
{
//如果要删除的是第一个节点,可以直接修改头指针
if(NULL == pre)
{
*head = (*head)->next;
}
else
{
pre->next = cur->next;
cur->next = NULL; //把要删除的节点从链表中分割开来
}
break; //删除一个节点以后结束
}
else //如果当前节点不是要删除的节点,pre和cur同时往后移动一个节点
{
pre = cur;
cur = cur->next;
}
}
//释放
if(NULL != cur)
{
free(cur);
}
}
//查找中间节点: 定义两个指针(一个快指针和一个慢指针),快指针一次移动两个节点,慢指针一次移动一个节点;当快指针到达链表末尾的时候,慢指针指向的就是中间节点
Link findMidNode(Link L)
{
//如果链表为空或者链表只有一个节点则没有中间节点
if(NULL == L || NULL == L->next)
{
return NULL;
}
//定义两个指针
Link slow = L;
Link fast = L;
//Link slow = L, fast = L; ==>Node *slow = L, fast = L;在这种定义中slow是Node*类型而fast是Node类型的;正确的定义方式: Node *slow = L, *fast = L;
//快慢指针同时遍历链表
while(NULL != fast)
{
fast = fast->next;
if(NULL == fast) //表示fast指针移动一个节点以后就到达了链表的末尾,这个时候慢指针是不需要移动的
{
break;
}
else
{
fast = fast->next;
}
slow = slow->next;
}
return slow; //慢指针指向的就是中间节点
}
//判定链表是否有环
_Bool isLoop(Link L)
{
if(NULL == L || NULL == L->next)
{
return false;
}
Link slow = L;
Link fast = L;
while(NULL != fast)
{
slow = slow->next;
fast = fast->next;
if(NULL == fast)
{
return false;
}
fast = fast->next;
//如果两个指针相遇,就表示这个位置在环中
if(slow == fast)
{
return true;
}
}
return false;
}
//偶数节点逆转
void reverseEvenNode(Link *L)
{
if(NULL == *L)
{
return;
}
Link head = *L; //指向奇数节点
Link temp = head->next; //指向偶数节点
while(NULL != temp && NULL != temp->next)
{
head->next = temp->next; //奇数节点连接偶数节点后面一个节点
temp->next = temp->next->next; //偶数节点连接其后面的第二个节点
head->next->next = temp; //偶数节点成为奇数节点后面的第二个节点
head = head->next->next;
temp = temp->next;
}
}
void show(Link L)
{
while(L)
{
printf("%5d", L->data);
L = L->next;
}
printf("\n");
}
//main.c
#include "link.h"
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
Link head = NULL;
//使用头插法来创建链表
insertHead(&head, 5);
show(head);
//使用尾插法来插入一个节点
insertTail(&head, 9);
show(head);
//随机插入: 在有序链表中插入
insertSortAsc(&head, 3);
insertSortAsc(&head, 7);
insertSortAsc(&head, 12);
show(head);
Link t = findMidNode(head);
printf("中间节点是%d\n", t->data);
reverseEvenNode(&head);
show(head);
return 0;
}
china@ubuntu$ gcc link.c main.c -o test
china@ubuntu$ ./test 5
5 9
3 5 7 9 12
中间节点是7
3 7 5 12 9
LeetCode146题:LRU
缓存算法:
a. FIFO(先进先出) -- 最早进入缓存的数据应该最早被淘汰掉
b. LRU(最近最少使用) -- 当缓存满的时候,优先淘汰那些最近没有使用的数据
c. LFU(最常使用) -- 当缓存满的时候,优先淘汰那些最不常使用的LRU的get和put要实现O(1)时间复杂度,可以使用双向链表保持缓存的时序,使用哈希表来定位元素的位置,实现O(1)
参考代码:
#include <iostream>
#include <unordered_map> //C++中STL里面哈希表的实现,可以将键映射到不同的桶中,快速查找对应的键
using namespace std;
class LRUCache
{
public:
class Node
{
public:
int key;
int val;
Node *pre;
Node *next;
Node()
{
}
Node(int mkey, int mval) : key(mkey), val(mval), pre(NULL), next(NULL)
{
}
};
//构造函数
LRUCache(int capacity)
{
size = capacity; //初始化空间大小为capacity
//头节点和尾节点:分配内存空间
head = new Node(-1, -1);
tail = new Node(-1, -1);
head->next = tail;
tail->pre = head;
}
//在双向链表中删除一个节点
Node *delete_currentnode(Node *current)
{
current->pre->next = current->next;
current->next->pre = current->pre;
return current;
}
//移动到最前面:相当于在双向链表中在head和head->next中插入当前节点
void move_to_head(Node *current)
{
Node *next = head->next;
head->next = current;
current->pre = head;
next->pre = current;
current->next = next;
}
void make_recently(Node *current)
{
Node *temp = delete_currentnode(current);
move_to_head(temp);
}
int get(int key)
{
int ret = -1;
if(map.find(key) != map.end())
{
Node *temp = map[key];
make_recently(temp);
ret = temp->val;
}
return ret;
}
void put(int key, int value)
{
if(map.find(key) != map.end())
{
//key存在
Node *temp = map[key];
temp->val = value;
//将当前节点变成最近使用过的节点(放到链表的第一个节点)
make_recently(temp);
}
else
{
//key不存在,直接插入键值对
Node *cur = new Node(key, value);
if(map.size() == size)
{
//将链表的最末尾(不是tail指向的节点,是tail前面那个节点)
Node *temp = delete_currentnode(tail->pre);
map.erase(temp->key);
}
move_to_head(cur);
map[key] = cur;
}
}
private:
int size; //表示缓存区的大小
unordered_map<int, Node *> map; //哈希表,是一个<int, Node *>的结构,int存储的是双向链表中某个节点的key,Node*指向双向链表中的对应节点
//双向链表的哨兵节点,一个指向双向链表的头,一个指向双向链表的尾
Node *head;
Node *tail;
};
int main()
{
LRUCache cache(2);
cache.put(1, 1);
cache.put(2, 2);
cout << cache.get(1) << endl;
cache.put(3, 3);
cout << cache.get(2) << endl;
cache.put(4, 4);
cout << cache.get(1) << ", " << cache.get(3) << ", " << cache.get(4) << endl;
return 0;
}