线性表上的数据最多只有前和后两个方向。
线性表数据结构
数组
数组用一组连续的内存空间,来存储一组具有相同类型的数据。
1. 优缺点
- 优点:支持随机访问,根据下标随机访问的时间复杂度为O(1)
- 缺点:插入、删除低效,平均时间复杂度为O(n)
2. 使用注意点
- 警惕访问越界
- 业务开发中可以使用容器代替数组,如果是底层开发有性能优化需要可以用数组
链表
链表通过指针将一组零散的内存块串联起来使用。
1. 链表分类
链表按结构可以分为:单链表、双向链表、循环列表等。
(1)单链表
链表的组成单元是head指针、结点,每个结点有数据值data和后继指针next,其中第一个结点叫作头结点,最后一个结点叫作尾结点,尾结点的指针指向空地址NULL。当head = NULL即为空链表。
- 优点:插入、删除高效,时间复杂度是O(1)
- 缺点:随机访问低效,时间复杂度是O(n)
(2)双向链表
每个结点不只有后继指针next,还有前驱指针prev,支持双向遍历,适合处理需要知道前驱结点的问题。
(3)循环列表
尾结点指针指向链表的头结点,适合处理具有环形结构的数据,例如“丢手绢”约瑟夫问题。
2. 链表代码注意点
(1)将某个变量赋值给指针,实际上就是将这个变量的地址赋值给指针:
- p->next=q表示p结点的next指针存储了q结点的内存地址;
- p->next=p->next->next表示p结点的next指针存储了p结点的下下一个结点的内存地址。
(2)警惕指针丢失和内存泄露
- 插入结点(new_node->next=p->next, p->next=new_node)时,注意先将被插入的结点指向下一个结点,再把当前结点指向被插入的结点,如下图和代码所示;
- 删除结点(p->next=p->next->next)时,注意手动释放内存空间。
// 在结点a和b之间插入结点x的写法(假设当前指针p指向结点a),如果颠倒过来就会发生指针丢失
x->next = p->next; // 第一步
p->next = x; // 第二步
(3)带头链表:引入哨兵结点(无数据、只有一个next指针),head指针会一直指向这个哨兵结点,就可以简化边界处理,对于插入第一个结点、删除最后一个结点的特殊情况无需特殊处理。
栈
栈先入后出,只能在一端插入和删除数据。
栈既可以用数组来实现、也可以用链表来实现,分别叫作顺序栈和链式栈。
栈的基本操作:入栈push、出栈pop,时间复杂度都是O(1),空间复杂度也是O(1)。
1. 栈的应用
函数调用栈
单调栈
从名字上就听的出来,单调栈中存放的数据应该是有序的,所以单调栈也分为:
- 单调递增栈:单调递增栈就是从栈底到栈顶数据是从大到小;
- 单调递减栈:单调递减栈就是从栈底到栈顶数据是从小到大。
单栈
- 检查表达式括号匹配:用栈来保存未匹配的左括号,从左到右依次扫描字符串,当扫描到左括号时压入栈,当扫描到右括号时,从栈顶取出一个左括号,如果能够匹配,则继续扫描剩下的字符串,如果遇到不能配对的右括号或栈中没有数据则为不匹配
双栈
- 表达式求值:双栈(操作数栈、操作符栈)。遇到数字就入数字栈;遇到操作符与操作符栈顶元素比较优先级,如果比栈顶元素优先级高,就将入操作符栈,如果优先级小于等于栈顶元素,就取数字栈栈顶的2个操作数和运算符栈栈顶的操作符进行计算,计算结果入数字栈。
- 浏览器的前进、后退:双栈X和Y,将顺序浏览的页面压入X,点后退时从X栈顶取网页压入Y,前进时再从Y栈顶取页面压入X
队列
队列先入先出,只支持两个基本操作:入队enqueue和出队dequeue。
队列既可以用数组来实现、也可以用链表来实现,分别叫作顺序队列和链式队列,另外还有循环队列。
1. 队列分类
(1)顺序队列
顺序队列是用数组来实现的队列。
- 需要双指针:head指向队头,tail指向队尾
- 为了避免没有空间了,入队时可以检测下tail是否指向数组尾,如果是的话进行整体的数据搬移,入队、出队的时间复杂度是O(1)
(2)链式队列
链式队列是用链表来实现的队列。
- 需要双指针:head指向第一个结点,tail指向最后一个结点
(3)循环队列
循环队列能够避免数据搬移。
- 判断队空和队满:队空head==tail,队满(tail+1)%n=head
2. 队列的业务应用
在资源有限的场景下,可以通过“队列”这种数据结构来实现请求排队。
(1)阻塞队列
在队空时,出队会被阻塞;
在队满时,入队会被阻塞。
(2)并发队列
线程安全的队列叫作并发队列,支持多个线程同时操作队列。