第一课:链表、栈、队列

文章目录

  • 第一周:链表、栈、队列
    • 1. 数组
      • vector可变长动态数组
    • 2. 链表
    • 3. 栈stack
    • 4. 队列queue
    • 5. 双端队列deque
    • 6. 优先队列priority queue
  • 实战

第一周:链表、栈、队列

1. 数组

数组操作时间复杂度:

  • Look up >> O(1)
  • Insert >> O(n)
  • Delete >> O(n)
  • Append (push back) >> O(1)
  • Prepend (push front) >> O(n)

vector可变长动态数组

特点:

  • 连续存储,空间可动态扩展
  • 随机访问[]vector.at()
  • 节省空间,但通常并不是满存
  • 内部插入、删除操作效率低,基本禁止。Vector 被设计成只能在后端进行追加和删除操作,其原因是vector 内部的实现是按照顺序表的原理,即只能push back 和pop back ,不能push front, pop front ;
  • 当动态添加的数据超过vector 默认分配的大小时要进行内存的重新分配、拷贝与释放,这个操作非常消耗性能。 所以要vector 达到最优的性能,最好在创建vector 时就指定其空间大小。

如何实现一个变长数组

  • 分配多长的连续空间?
    • 空数组,分配常数空间
  • Push back空间不够用怎么处理?
    • 申请2倍大小连续空间 >> 拷贝 >> 销毁原对象 >> 释放旧空间
  • Pop back空间剩余超过75%怎么处理?
    • 释放一半的空间

时间复杂度:

  • 均摊O(1)
  • 插入、删除O(n)

思考:若释放空间阀值设为50%,会怎么样

  • 当空间占用为临界阀值附近时,会在扩容和减半操作之间来回震荡

声明初始化一个vector

#include

vector vec;		//声明一个int型向量
vector vec(5);		//声明一个初始大小为5的int向量
vector vec(10, 1);	//声明一个初始大小为10且值都是1的向量
vector vec(tmp);	//声明并用tmp向量初始化vec向量
vector tmp(vec.begin(), vec.begin() + 3);	//用向量vec的第0个到第2个值初始化tmp
int arr[5] = {1, 2, 3, 4, 5};	
vector vec(arr, arr + 5);		//将arr数组的元素用于初始化vec向量
//说明:当然不包括arr[4]元素,末尾指针都是指结束元素的下一个元素,
//这个主要是为了和vec.end()指针统一。
vector vec(&arr[1], &arr[4]); //将arr[1]~arr[4]范围内的元素作为vec的初始值

基本操作

(1). 容量

  • 向量大小: vec.size();
  • 向量最大容量: vec.max_size();
  • 更改向量大小: vec.resize();
  • 向量真实大小: vec.capacity();
  • 向量判空: vec.empty();
  • 减少向量大小到满足元素所占存储空间的大小: vec.shrink_to_fit(); //shrink_to_fit

(2). 修改

  • 多个元素赋值: vec.assign(); //类似于初始化时用数组进行赋值
  • 末尾添加元素: vec.push_back();
  • 末尾删除元素: vec.pop_back();
  • 任意位置插入元素: vec.insert();
  • 任意位置删除元素: vec.erase();
  • 交换两个向量的元素: vec.swap();
  • 清空向量元素: vec.clear();

(3)迭代器

  • 开始指针:vec.begin();
  • 末尾指针:vec.end(); //指向最后一个元素的下一个位置
  • 指向常量的开始指针: vec.cbegin(); //意思就是不能通过这个指针来修改所指的内容,但还是可以通过其他方式修改的,而且指针也是可以移动的。
  • 指向常量的末尾指针: vec.cend();

(4)元素的访问

  • 下标访问: vec[1]; //并不会检查是否越界
  • at方法访问: vec.at(1); //以上两者的区别就是at会检查是否越界,是则抛出out of range异常
  • 访问第一个元素: vec.front();
  • 访问最后一个元素: vec.back();
  • 返回一个指针: int* p = vec.data(); //可行的原因在于vector在内存中就是一个连续存储的数组,所以可以返回一个指针指向这个数组。这是是C++11的特性。

算法

// 遍历
vector::iterator it;
for (it = vec.begin(); it != vec.end(); it++)
    cout << *it << endl; // meanue1
for (size_t i = 0; i < vec.size(); i++) {
	cout << vec.at(i) << endl; // meanuel2
}
// 元素翻转
#include 
reverse(vec.begin(), vec.end());
// 元素排序
#include 
sort(vec.begin(), vec.end()); //采用的是从小到大的排序
//如果想从大到小排序,可以采用上面反转函数,也可以采用下面方法:
bool Comp(const int& a, const int& b) {
	return a > b;
}
sort(vec.begin(), vec.end(), Comp);

2. 链表

知识点简单,主要是多练习熟悉一下。

数组插入删除很贵O(n),引入链表

首先加入保护节点,哨兵节点

ListNode protect = new ListNode(0, head); // (value, ->next)

双向链表,双保护节

时间复杂度:

  • look up >> O(n)
  • Insert >> O(1)
  • Delete >> O(1)
  • Append(push back) >> O(1)
  • Prepend(push front) >> O(1)

3. 栈stack

时间复杂度

  • push 入栈 : O(1)
  • pop 出栈 : O(1)
  • Access 访问栈顶 : O(1)

只能访问、push、pop顶部的元素

操作

  • top():返回一个栈顶元素的引用,类型为 T&。如果栈为空,返回值未定义。
  • push(const T& obj):可以将对象副本压入栈顶。这是通过调用底层容器的 push_back() 函数完成的。
  • push(T&& obj):以移动对象的方式将对象压入栈顶。这是通过调用底层容器的有右值引用参数的 push_back() 函数完成的。
  • pop():弹出栈顶元素。
  • size():返回栈中元素的个数。
  • empty():在栈中没有元素的情况下返回 true。
  • emplace():用传入的参数调用构造函数,在栈顶生成对象。
  • swap(stack & other_stack):将当前栈中的元素和参数中的元素交换。参数所包含元素的类型必须和当前栈的相同。对于 stack 对象有一个特例化的全局函数 swap() 可以使用。
#include
#include 

stack mystack1;
stack > mystack2;

4. 队列queue

时间复杂度

  • push 入队 : O(1)
  • pop 出队 : O(1)
  • Access 访问队头 : O(1)
#include
#include

queue m;
queue > n;

操作

  • front():返回 queue 中第一个元素的引用。如果 queue 是常量,就返回一个常引用;如果 queue 为空,返回值是未定义的。
  • back():返回 queue 中最后一个元素的引用。如果 queue 是常量,就返回一个常引用;如果 queue 为空,返回值是未定义的。
  • push(const T& obj):在 queue 的尾部添加一个元素的副本。这是通过调用底层容器的成员函数 push_back() 来完成的。
  • push(T&& obj):以移动的方式在 queue 的尾部添加元素。这是通过调用底层容器的具有右值引用参数的成员函数 push_back() 来完成的。
  • pop():删除 queue 中的第一个元素。
  • size():返回 queue 中元素的个数。
  • empty():如果 queue 中没有元素的话,返回 true。
  • emplace():用传给 emplace() 的参数调用 T 的构造函数,在 queue 的尾部生成对象。
  • swap(queue &other_q):将当前 queue 中的元素和参数 queue 中的元素交换。它们需要包含相同类型的元素。也可以调用全局函数模板 swap() 来完成同样的操作。

5. 双端队列deque

时间复杂度O(1)

img

一般队列以“时间”为顺序(先进先出)

优先队列按照元素的“优先级”取出

  • ”优先级“可以使自己定义的一个元素属性

许多数据结构可以用来实现优先队列,例如二叉堆、二叉平衡树等

deque的API

  • 1.1deque构造函数
    deque deqT;//默认构造形式

    deque(beg, end);//构造函数将[beg, end)区间中的元素拷贝给本身。

    deque(n, elem);//构造函数将n个elem拷贝给本身。

    deque(const deque &deq);//拷贝构造函数。

  • ==1.2 deque赋值操作
    assign(beg, end);//将[beg, end)区间中的数据拷贝赋值给本身。

    assign(n, elem);//将n个elem拷贝赋值给本身。

    deque& operator=(const deque &deq); //重载等号操作符

    swap(deq);// 将deq与本身的元素互换

  • 1.3 deque大小操作
    deque.size();//返回容器中元素的个数

    deque.empty();//判断容器是否为空

    deque.resize(num);//重新指定容器的长度为num,若容器变长,则以默认值填充新位置。如果容器变短,则末尾超出容器长度的元素被删除。

    deque.resize(num, elem); //重新指定容器的长度为num,若容器变长,则以elem值填充新位置,如果容器变短,则末尾超出容器长度的元素被删除。

//遍历容器
void printDeque(const deque &d)
{
    for(deque::const_iterator it = d.begin(); it != d.end(); it++)
    {
        cout<<*it<<" ";
    }
    cout<

2.1 deque双端插入和删除操作
push_back(elem);//在容器尾部添加一个数据

push_front(elem);//在容器头部插入一个数据

pop_back();//删除容器最后一个数据

pop_front();//删除容器第一个数据

2.2 deque数据存取
at(idx);//返回索引idx所指的数据,如果idx越界,抛出out_of_range。

operator[];//返回索引idx所指的数据,如果idx越界,不抛出异常,直接出错。

front();//返回第一个数据。

back();//返回最后一个数据

2.3 deque插入操作
insert(pos,elem);//在pos位置插入一个elem元素的拷贝,返回新数据的位置。

insert(pos,n,elem);//在pos位置插入n个elem数据,无返回值。

insert(pos,beg,end);//在pos位置插入[beg,end)区间的数据,无返回值。

2.4 deque删除操作
clear();//移除容器的所有数据

erase(beg,end);//删除[beg,end)区间的数据,返回下一个数据的位置。

erase(pos);//删除pos位置的数据,返回下一个数据的位置。

6. 优先队列priority queue

时间复杂度

  • 访问最值:O(1)

  • 插入:一般是O(logN)

  • 去最值:O(logN)

定义:priority_queue
Type 就是数据类型,Container 就是容器类型(Container必须是用数组实现的容器,比如vector,deque等等,但不能用 list。STL里面默认用的是vector),Functional 就是比较的方式,当需要用自定义的数据类型时才需要传入这三个参数,使用基本数据类型时,只需要传入数据类型,默认是大顶堆

// 升序队列
priority_queue ,greater > q;
// 降序队列
priority_queue ,less >q;
// greater和less是std实现的两个仿函数(就是使一个类的使用看上去像一个函数。其实现就是类中实现一个operator(),这个类就有了类似函数的行为,就是一个仿函数类了)

//对于基础类型 默认是大顶堆
priority_queue a; 
//等同于 priority_queue, less > a;

和队列基本操作相同:

  • top 访问队头元素
  • empty 队列是否为空
  • size 返回队列内元素个数
  • push 插入元素到队尾 (并排序)
  • emplace 原地构造一个元素并插入队列
  • pop 弹出队头元素
  • swap 交换内容

实战

数组

  • 88.合并有序数组 easy
    https://leetcode-cn.com/problems/merge-sorted-array/

    • 两个数组从后往前的指针开始比较,谁大放谁,放入后并----相应的指针

    • 防止被覆盖,从后开始放最大的

    • 边界问题

      //一个数什么时候要的模板(重复)
      int n = 0;
      for (int i = 0; i < nums.size(); i++){
          if (i == 0 || nusm[i] != nums[i - 1]){
              nums[n] = nums[i];
              n++;
          }
      }
      
  • 26.去重 easy
    https://leetcode-cn.com/problems/remove-duplicates-from-sorted-array/

    • 前后指针
    • 建立循环, 前指针每次都++:
    • 当前指针=后指针,后指针不变
    • 前指针!=后指针,后指针 = 前指针,后指针++
    • 边界问题,注意考虑前后指针都为0的情况
  • 283.移动零 easy
    https://leetcode-cn.com/problems/move-zeroes/

    • 建立新指针,从前往后遍历,不是0就要,指针++
    • 如果n小于nums.size(),后面补0;
    • 第二种思路:
    • 双指针,快指针每次都++
    • 当快指针扫描到非0,慢指针扫描到0时,交换位置

链表

  • 临值查找 hard
    https://www.acwing.com/problem/content/description/138/

  • 141.环形链表 easy
    https://leetcode-cn.com/problems/linked-list-cycle/

    • 快慢指针
    • 相遇了则返回有环
    • 不相遇返回无环
  • 142.环形链表II mid
    https://leetcode-cn.com/problems/linked-list-cycle-ii/

    • 根据:
      fast = 2s (快指针每次2步,路程刚好2倍)
      fast = s + nb (相遇时,刚好多走了n圈)
      推出:slow = nb
      1.第一次相遇,slow = nb
      2.入环点 = a + nb = slow + a
      所以说想再次到入环点,slow指针还要走a。
      如何知道slow刚好走了a步? 从head开始,和slow指针一起走,相遇时刚好就是a步
  • 206.反转链表 easy
    https://leetcode-cn.com/problems/reverse-linked-list/

    • 注意最后的指向null的指针

    • next指针改为指向last的,但是链表操作中没有last指针,开一个新的指针记录last

    • 最后返回的是指针

    • //遍历链表:
      while (head != nullptr){
          head = head->next // 如果需要更改,新建一个指针暂存
      }
      
  • 25.K个一组翻转链表 hard
    https://leetcode-cn.com/problems/reverse-nodes-in-k-group/

    • 分组,找到每一组的开始、结尾。按组遍历

    • // 找到一组k的结尾
      private ListNode getEnd(ListNode head, intk){
          while (head != null){
              k--;
              if (k == 0)break;
              return head;
          }
      }
      
    • 反转一组

    • // 反转两个节点之间的链表
      private void reverseList(ListNode head, ListNode end){
          if (head == end) return;
          ListNode last = head;
          head = head->next
          // 改变每条边,访问链表
          while (head != end){
              ListNode nextHead = last;
              // 改变一条边
      		head->next = last;
              // last, head的地址向后移动一位
              last = head;
              head = nextHead;
          }
          end->next = last;
      }
      
    • 上一组跟本组的head(旧的end)建立联系

    • 让本组的新结尾(end)跟下一组建立联系

    • 边界问题

  • 20.有效的括号 easy
    https://leetcode-cn.com/problems/valid-parentheses/

    • 建立一个栈
    • 左括号push,右括号和top比较。不对应就false,对应就pop
    • 注意if嵌套关系,和判断pop后的空栈情况
  • 155.最小栈 medium
    https://leetcode-cn.com/problems/min-stack/

    • 新建两个栈,一个正常栈操作,一个使用前缀最小值
    • 前缀最小值:新开栈,遍历数组和栈顶比,小于栈顶就进栈,大于就下一个
  • 150.逆波兰式 后缀表达式求值 medium
    https://leetcode-cn.com/problems/evaluate-reverse-polish-notation/

    • 新建栈
    • 遍历数组,是数字进栈,是运算符连续pop两个数字进行运算
  • 227 medium &224 hard 基本计算器 中缀表达式
    https://leetcode-cn.com/problems/basic-calculator-ii/
    https://leetcode-cn.com/problems/basic-calculator/

    • // 字符串转数值模板
      long long val = 0;
      for (char ch : s){
      	if (ch >='0' && ch <= '9'){
      		val = val * 10 + ch - '0';
      	}
      }
      
    • 定义优先级

    • 转后缀,再求解

第一课:链表、栈、队列_第1张图片 第一课:链表、栈、队列_第2张图片

homework

  • 21.合并两个有序链表 easy
    https://leetcode-cn.com/problems/merge-two-sorted-lists/

    • 创建哨兵节点,和一个pre指针
    • 遍历l1和l2直到有一个为nullptr
    • l1和l2的头节点比较,pre->next指向较小的(假如为l1)
    • l1递归,l1 = l1->next
    • 将l1或者l2剩下的接到最后
  • 66.加一 easy
    https://leetcode-cn.com/problems/plus-one/

    • 从后往前遍历,分三种情况
      1. 最后一个不是9,直接最后一位+1,break返回
      2. 最后一个是9,变为0,继续往前扫描
      3. 直到第一个数,如果其为9,创建一个新vector长度+1,第一个数为1其余的都为0;不是9的话+1
  • 641.设置循环双端队列 medium
    https://leetcode-cn.com/problems/design-circular-deque/

你可能感兴趣的:(数据结构与算法,数据结构,算法)