第二次
本文内容由简易程序从latex批量转义而来, 排版并不友好, 相对精美版请见课程整理
实验 2: 线性表实现及应用
%\addcontentsline{toc}{chapter}{实验 1: 渐进分析和排序算法}
Task 1: 为指定的 List ADT 实现各种数据结构
代码实现
\subsubsection{顺序数组}
template<typename T>
class List_ADT{
int cursor, len;
vector<T> val;
public:
void init(int length)
{
val.resize(length);
cursor = -1;
len = 0;
return;
}
void insert(T newElement)
{
if(isFull())
{
printf("Error! Out of the list capacity!\n");
return;
}
for(int i = len;i > cursor + 1; i--)
val[i] = val[i - 1];
len ++;
val[++ cursor] = newElement;
}
void remove()
{
if(isEmpty()) return;
len --;
for(int i = cursor; i < len; i ++)
val[i] = val[i + 1];
cursor = cursor == len ? 0 : cursor;
if(len == 0)
cursor = -1;
return;
}
void replace(T newElement)
{
if(isEmpty()) return;
val[cursor] = newElement;
return;
}
void clear()
{
len = 0;
cursor = -1;
return;
}
bool isEmpty()
{
return len == 0;
}
bool isFull()
{
return len == (int)val.size();
}
bool gotoBeginning()
{
if(isEmpty()) return false;
else
{
cursor = 0;
return true;
}
}
bool gotoEnd()
{
if(isEmpty()) return false;
else
{
cursor = len - 1;
return true;
}
}
bool gotoNext()
{
if(cursor + 1 != len)
{
cursor ++;
return true;
}
else return false;
}
bool gotoPrev()
{
if(isEmpty()) return false;
else if(cursor == 0) return false;
else
{
cursor --;
return true;
}
}
T getCursor()
{
if(isEmpty()) return 0;
return val[cursor];
}
void showStructure()
{
if(isEmpty()) printf("Empty list ");
else
{
for(int i=0;i<len;i++)
putchar(val[i]),putchar(' ');
}
printf("{capacity = %d, length = %d, cursor = %d}\n",(int)val.size(),len,cursor);
}
void moveToNth(int n)
{
T tmp = getCursor();
remove();
cursor = n - 1;
insert(tmp);
}
bool find(T searchElement)
{
while(getCursor() != searchElement&&gotoNext());
return getCursor() == searchElement;
}
};
char s[100010];
int main()
{
freopen("list_testcase.txt","r",stdin);
freopen("数组.txt","w",stdout);
List_ADT<char> a;
a.init(512);
while(gets(s) != NULL)
{
int n = strlen(s);
for(int i = 0; i < n; i ++)
{
if(s[i] == ' ') continue;
if(s[i] == '+')
a.insert(s[++i]);
else if(s[i] == '-')
a.remove();
else if(s[i] == '=')
a.replace(s[++ i]);
else if(s[i] == '#')
a.gotoBeginning();
else if(s[i] == '*')
a.gotoEnd();
else if(s[i] == '>')
a.gotoNext();
else if(s[i] == '<')
a.gotoPrev();
else if(s[i] == '~')
a.clear();
}
a.showStructure();
}
return 0;
}
\subsubsection{单向链表}
主函数同上, 此处不多赘述.
template<typename T>
class List_ADT{
int cursor, len, lenLimit;
struct Point{
T val;
Point *next;
};
Point *head = nullptr, *p = nullptr, *tmp = nullptr;
public:
void init(int length)
{
head = nullptr;
cursor = -1;
len = 0;
lenLimit = length;
return;
}
void insert(T newElement)
{
if(isFull())
{
printf("Error! Out of the list capacity!\n");
return;
}
if(len == 0)
{
head = p = new Point;
p -> next = nullptr;
p -> val = newElement;
}
else
{
tmp = p -> next;
p -> next = new Point;
p = p -> next;
p -> val = newElement;
p -> next = tmp;
}
len ++;
cursor ++;
return;
}
void remove()
{
if(isEmpty()) return;
if(p == head)
{
p = head -> next;
free(head);
head = p;
}
else
{
tmp = p;
gotoPrev();
p -> next = tmp -> next;
free(tmp);
p = p -> next;
cursor ++;
if(p == nullptr) p = head, cursor = 0;
}
len --;
if(len == 0)
{
cursor = -1;
head = p = nullptr;
}
return;
}
void replace(T newElement)
{
if(isEmpty()) return;
p->val = newElement;
return;
}
void clear()
{
len = 0;
cursor = -1;
p = head;
while(p!= nullptr)
{
tmp = p;
p = p -> next;
free(tmp);
}
head = p = nullptr;
return;
}
bool isEmpty()
{
return len == 0;
}
bool isFull()
{
return len == lenLimit;
}
bool gotoBeginning()
{
if(isEmpty()) return false;
else
{
cursor = 0;
p = head;
return true;
}
}
bool gotoEnd()
{
if(isEmpty()) return false;
else
{
gotoBeginning();
while(gotoNext());
return true;
}
}
bool gotoNext()
{
if(isEmpty()) return false;
if(p -> next != nullptr)
{
cursor ++;
p = p -> next;
return true;
}
else return false;
}
bool gotoPrev()
{
if(isEmpty()) return false;
if(p == head) return false;
tmp = p;
gotoBeginning();
while(p -> next != tmp) gotoNext();
return true;
}
T getCursor()
{
if(isEmpty()) return nullptr;
return p -> val;
}
void showStructure()
{
if(isEmpty()) printf("Empty list ");
else
{
tmp = head;
while(tmp != nullptr)
{
putchar(tmp -> val);
putchar(' ');
tmp = tmp -> next;
}
}
printf("{capacity = %d, length = %d, cursor = %d}\n",lenLimit,len,cursor);
}
void moveToNth(int n)
{
T tmp = getCursor();
remove();
gotoBeginning();
for(int i = 1; i < n; i ++)
gotoNext();
insert(tmp);
}
bool find(T searchElement)
{
while(getCursor() != searchElement && gotoNext());
return getCursor() == searchElement;
}
};
\subsubsection{双向链表}
template<typename T>
class List_ADT{
int cursor, len, lenLimit;
struct Point{
T val;
Point *next,*pre;
};
Point *head = nullptr, *p = nullptr, *tmp = nullptr;
public:
void init(int length)
{
head = p = nullptr;
cursor = -1;
len = 0;
lenLimit = length;
return;
}
void insert(T newElement)
{
if(isFull())
{
printf("Error! Out of the list capacity!\n");
return;
}
if(len == 0)
{
head = p = new Point;
p -> next = p -> pre = nullptr;
p -> val = newElement;
}
else
{
tmp = p -> next;
p -> next = new Point;
p -> next -> pre = p;
p = p -> next;
p -> val = newElement;
p -> next = tmp;
if(tmp != nullptr)
tmp -> pre = p;
}
len ++;
cursor ++;
return;
}
void remove()
{
if(isEmpty()) return;
if(p == head)
{
p = head -> next;
free(head);
head = p;
if(p != nullptr)
p -> pre = nullptr;
}
else
{
tmp = p;
p -> pre -> next = p -> next;
if(p -> next != nullptr)
p -> next -> pre = p -> pre;
p = p -> next;
free(tmp);
if(p == nullptr) p = head, cursor = 0;
}
len --;
if(len == 0)
{
cursor = -1;
head = p = nullptr;
}
return;
}
void replace(T newElement)
{
if(isEmpty()) return;
p->val = newElement;
return;
}
void clear()
{
len = 0;
cursor = -1;
p = head;
while(p!= nullptr)
{
tmp = p;
p = p -> next;
free(tmp);
}
head = p = nullptr;
return;
}
bool isEmpty()
{
return len == 0;
}
bool isFull()
{
return len == lenLimit;
}
bool gotoBeginning()
{
if(isEmpty()) return false;
else
{
cursor = 0;
p = head;
return true;
}
}
bool gotoEnd()
{
if(isEmpty()) return false;
else
{
while(gotoNext());
return true;
}
}
bool gotoNext()
{
if(isEmpty()) return false;
if(p -> next != nullptr)
{
cursor ++;
p = p -> next;
return true;
}
else return false;
}
bool gotoPrev()
{
if(isEmpty()) return false;
if(p == head) return false;
p = p -> pre;
cursor --;
return true;
}
T getCursor()
{
if(isEmpty()) return nullptr;
return p -> val;
}
void showStructure()
{
if(isEmpty()) printf("Empty list ");
else
{
tmp = head;
while(tmp != nullptr)
{
putchar(tmp -> val);
putchar(' ');
tmp = tmp -> next;
}
}
printf("{capacity = %d, length = %d, cursor = %d}\n",lenLimit,len,cursor);
}
void moveToNth(int n)
{
T tmp = getCursor();
remove();
gotoBeginning();
for(int i = 1; i < n; i ++)
gotoNext();
insert(tmp);
}
bool find(T searchElement)
{
while(getCursor() != searchElement && gotoNext());
return getCursor() == searchElement;
}
};
以上代码均通过实验中提供的测试用例, 利用系统自带的 \(\text{fc}\) 比较上述代码输出文件于实验下发文件, 均返回无差异.
对于实验题目注 \(\text{List}\) 实现的存储方式为链式结构, 其 \(\text{capacity}\) 应为真实结点个数, 其实就应该等于输出中的 \(\text{len}\) 的值, 所以在测试链表时, 只需在 \(\text{capacity}\) 处输出 \(512\) 并直接与结果比较即可.
代码差异分析
仔细研究各函数的写法, 我们可以找到一些“本质”的函数, 此处认为与存储方式有关的函数是“本质”的. 比如 \(\text{moveToNth,find,gotoEnd}\) 这些函数就是非“本质”的.
也就是说这些函数的实现和存储方式无关, 可以由其它函数多次调用实现, 对于上述三种存储表示其实现的代码是相同的.
而为了简化代码的实现, 我们希望“本质”的函数尽量少, 这样在换用不同的存储逻辑实现相同的操作时, 我们需要对代码的改动就少.
更进一步的, 单向链表和双向链表的差异很小, 对于这两种存储逻辑, 需要修改的部分其实只有 \(\text{gotoPrev}\).
不同存储方式复杂度分析
$$
\begin{array} {|c|c|c|c|}\hline
\text{函数名称} & \text{顺序数组} & \text{单向链表} & \text{双向链表} \\hline
\text{insert} & \text O(n) & \text O(1) & \text O(1) \\hline
\text{remove} & \text O(n) & \text O(n) & \text O(1) \\hline
\text{replace} & \text O(1) & \text O(1) & \text O(1) \\hline
\text{clear} & \text O(1) & \text O(n) & \text O(n) \\hline
\text{isEmpty} & \text O(1) & \text O(1) & \text O(1) \\hline
\text{isFull} & \text O(1) & \text O(1) & \text O(1) \\hline
\text{gotoBeginning} & \text O(1) & \text O(1) & \text O(1) \\hline
\text{gotoEnd} & \text O(1) & \text O(n) & \text O(n) \\hline
\text{gotoNext}& \text O(1) & \text O(1) & \text O(1) \\hline
\text{gotoPrev}& \text O(1) & \text O(n) & \text O(1) \\hline
\text{getCursor} & \text O(1) & \text O(1) & \text O(1) \\hline
\text{showStructure} & \text O(n) & \text O(n) & \text O(n) \\hline
\text{moveToNth} & \text O(n) & \text O(n) & \text O(n) \\hline
\text{find} & \text O(n) & \text O(n) & \text O(n) \\hline
\end{array}
$$
一些小细节及分析:
(1) 对于 \(\text{clear}\) 的复杂度, 此处考虑到要将单向/双向链表新建的结点空间释放, 需要遍历整个链表所以其复杂度为 \(\text O(n)\). 相应的, 对于数组的清空, 我们只需清楚光标和长度即可, 因为数组的空间是在最初始申请的, 不会随着程序运行而变化, 而且每次操作都是赋值, 不清空也不会对后续操作产生影响.
(2) 对于 \(\text{moveTonNth}\) 操作, 虽然三种存储用时均为 \(\text O(n)\), 但其具体的原因却不相同, 数组是因为插入删除操作需要 \(\O(n)\) 的时间, 寻找第 \(n\) 是 \(\text O(1)\), 所以该操作用时为 \(\text O(n)\). 而对于双向链表, 其主要复杂度用于寻找第 \(n\) 个位置, 而相应的插入删除操作则仅仅为 \(\text O(1)\).
(3) 对于单向链表和双向链表, 其本质差距只有 \(\text{gotoPrev}\) 这个函数的时间消耗, 双向链表可以 \(\text O(1)\) 查询前驱, 进而在 \(\text O(1)\) 的复杂度完成删除操作.
(4) 如果我们在链表处理时在动态维护一个尾指针, 那么我们可以将 \(\text{gotoEnd}\) 的复杂度优化至 \(\text O(1)\), 但不会使其他操作用时间增加.
(5) 如果进行 (4) 的优化, 仅通过上述表格, 我们认为数组远不如链表, 因为链表的每一项操作都优于数组. 但实际上正如 (2) 所述, 数组的优势其实是可以在 \(\text O(1)\) 的时间内随机访问, 而相应的链表则需要 \(\text O(n)\) 的时间, 而这种访问操作并没有体现在上述测试过程中. 并且该操作在很多实际应用中占了大部分, 所以数组在这方面优势很大. 这两类存储应该说各有优劣, 要根据不同的应用场景进行选择.
Task 2: 为指定的 DQueue ADT 实现两种数据结构
代码实现
\subsubsection{顺序数组}
利用循环数组实现.
template<typename T>
class DQueue{
vector<T>q;
int siz, head, tail, sizeLimit;
T tmp;
public:
void init(int length)
{
q.resize(length);
sizeLimit = length;
siz = 0;
head = 0;
tail = -1;
}
bool isEmpty()
{
return siz == 0;
}
bool isFull()
{
return siz == sizeLimit;
}
void clear()
{
siz = 0;
head = 0;
tail = -1;
}
int size()
{
return siz;
}
void enqueueToRear(T element)
{
if(isFull()) return;
tail = (tail + 1) % sizeLimit;
q[tail] = element;
++ siz;
return;
}
void enqueueToFront(T element)
{
if(isFull()) return;
head = (head + sizeLimit - 1) % sizeLimit;
q[head] = element;
++ siz;
return;
}
T dequeueFromFront()
{
if(isEmpty()) return {};
tmp = q[head];
head = (head + 1) % sizeLimit;
-- siz;
return tmp;
}
T dequeueFromRear()
{
if(isEmpty()) return {};
tmp = q[tail];
tail = (tail + sizeLimit - 1) % sizeLimit;
-- siz;
return tmp;
}
T getFront()
{
if(isEmpty()) return {};
return q[head];
}
T getRear()
{
if(isEmpty()) return {};
return q[tail];
}
};
\subsubsection{双向链表}
template<typename T>
class DQueue{
struct Point{
T val;
Point *next, *pre;
};
int siz, sizeLimit;
T tmpval;
Point *head, *tail, *tmp;
public:
void init(int length)
{
sizeLimit = length;
siz = 0;
head = tail = nullptr;
}
void clear()
{
while(head != nullptr)
{
tmp = head;
head = head -> next;
free(tmp);
}
siz = 0;
head = tail = nullptr;
}
bool isEmpty()
{
return siz == 0;
}
bool isFull()
{
return siz == sizeLimit;
}
int size()
{
return siz;
}
void enqueueToRear(T element)
{
if(isFull()) return;
if(isEmpty())
{
tail = head = new Point;
head -> val = element;
head -> pre = head -> next = nullptr;
}
else
{
tail -> next = new Point;
tail -> next -> pre = tail;
tail = tail -> next;
tail -> val = element;
tail -> next = nullptr;
}
++ siz;
return;
}
void enqueueToFront(T element)
{
if(isFull()) return;
if(isEmpty())
{
tail = head = new Point;
head -> val = element;
head -> pre = head -> next = nullptr;
}
else
{
head -> pre = new Point;
head -> pre -> next = head;
head = head -> pre;
head -> val = element;
head -> pre = nullptr;
}
++ siz;
return;
}
T dequeueFromFront()
{
if(isEmpty()) return {};
tmp = head;
tmpval = tmp -> val;
head = head -> next;
free(tmp);
-- siz;
return tmpval;
}
T dequeueFromRear()
{
if(isEmpty()) return {};
tmp = tail;
tmpval = tmp -> val;
tail = tail -> pre;
free(tmp);
-- siz;
return tmpval;
}
T getFront()
{
if(isEmpty()) return {};
return head -> val;
}
T getRear()
{
if(isEmpty()) return {};
return tail -> val;
}
};
滑动窗口问题
\subsubsection{问题分析}
此问题可以采用单调队列这一算法完成. 考虑到靠后的较大值, 肯定比前面的较小值更优, 因为再取最大值的时候, 当靠后的值更大时, 我们可以直接忽略前面比它小的值.
基于此思想, 我们区别于直接入队, 而是先把之前队列中比即将要插入的值小的值从队尾弹出. 然后再将当前值从队尾插入. 利用归纳的思想, 不难证明在上述操作过程中, 我们的这个队列始终保持一个单调性. 即从队首到队尾的值是单调递减的.
而对于滑动窗口这个问题, 我们还需要记录队列中每个值的位置, 而当队首元素的位置超出当前计算的区间时, 就需要将其从队首弹出.
由此, 我们就需要之前实现过的双端队列 \(\text{DQueue}\) 数据结构来完成这些操作.
而对于存储位置, 我们有两种操作, 一种是直接在 \(\text{DQueue}\) 中放入自定义结构体类型, 而每个结构体变量都包含值及其位置, 另一种是, 在 \(\text{DQueue}\) 中放入位置, 然后每次根据位置去调取该位置上的值再进行比较.
\subsubsection{代码实现}
以下代码中的 \(\text{DQueue}\) 类, 就是上述列举的两个代码, 并且下述代码均通过了洛谷 \(\text{P1886}\) 滑动窗口 /【模板】单调队列 题目. 第一行输出为每个区间的最小值, 第二行输出为最大值.
struct node{
int pos, val;
};
int n, k;
vector<int>a;
int main()
{
read(n), read(k);
DQueue<node> q;
a.resize(n + 2);
q.init(n + 2);
for(int i = 1; i <= n; i ++)
{
read(a[i]);
while(!q.isEmpty() && i - q.getFront().pos >= k) q.dequeueFromFront();
while(!q.isEmpty() && q.getRear().val >= a[i]) q.dequeueFromRear();
q.enqueueToRear({i,a[i]});
if(i >= k) write(q.getFront().val, i != n ? ' ' : '\n');
}
q.clear();
for(int i = 1; i <= n; i ++)
{
while(!q.isEmpty() && i - q.getFront().pos >= k) q.dequeueFromFront();
while(!q.isEmpty() && q.getRear().val <= a[i]) q.dequeueFromRear();
q.enqueueToRear({i,a[i]});
if(i >= k) write(q.getFront().val, i != n ? ' ' : '\n');
}
flushout();
return 0;
}
Task 3: 栈
快排非递归转化
\subsubsection{问题分析}
要进行非递归转化, 我们需要关注快速排序在递归过程中哪些值被传递到了下一层.
发现, 快速排序实际上只用传输区间端点 \(l,r\) 这两个值, 所以我们可以利用栈, 每一层就存放这两个值, 然后在处理完当层之后, 将下一层递归的区间加入栈即可.
\subsubsection{代码实现}
int part(vector<int>&num,int l,int r)
{
if(l == r) return -1;
int pos = random(l,r);
int p = num[pos];
swap(num[pos],num[r]);
int i = l, j = r - 1;
while(i < j)
{
while(i < j&& num[i] < p) i ++;
while(i < j&& num[j] >= p) j --;
swap(num[i], num[j]);
}
if(num[i] >= num[r]) swap(num[i], num[r]);
else i ++;
return i;
}
void Quicksort(vector<int>&num)
{
stack<pii> stk;
stk.push({0, num.size() - 1});
while(stk.size())
{
pii p = stk.top();
stk.pop();
int i = part(num,p.first,p.second);
if(i == -1)continue;
if(p.first < i - 1) stk.push({p.first, i - 1});
if(i + 1 < p.second) stk.push({i + 1, p.second});
}
return;
}
算术混合运算表达式计算
\subsubsection{问题分析及算法设计}
利用栈, 将中缀表达式转换为后缀表达式(逆波兰表达式), 并进行计算.
由于本题只要求最终结果, 所以并未求出完整的后缀表达式, 而是在计算过程中同步计算.
先不考虑括号.
具体思路, 考虑给每种运算符赋予一个优先级, 我们用两个栈分别维护运算符和数值, 而当之前压入栈的运算符优先级小于等于当前运算符时, 就将之前的运算符出栈并进行计算. 计算后再将当前的运算符压入栈.
而当多了括号也很简单, 在遇到左括号的时候, 我们只需将其入栈, 不需要任何额外操作. 当遇到右括号时, 我们一直做出栈操作, 直至栈顶为左括号.
至于出栈操作, 具体点就是取出运算符的栈顶和数值栈顶两个元素, 将这两个元素做相应的运算. 然后再将结果压入数值栈.
如此操作之后, 当最终运算符栈为空, 数值栈只剩一个元素时, 这个元素就是表达式的结果.
注: 一些细节, 当 '-' 号前为空或者是左括号时, 此时 '-' 号应理解为负号, 而不是减号. 需要特殊处理, 为了统一, 实际上此时只需在数值栈压入一个 \(0\), 从而将 '-' 号视作减号即可.
\subsubsection{代码实现}
const int N = 2e5 + 10;
char s[N];
int n, stk_p[N],top_p,top_val;
double stk_val[N];
int pr(char opt)
{
switch(opt)
{
case '+':
return 1;
case '-':
return 1;
case '*':
return 2;
case '/':
return 2;
case '^':
return 3;
}
return 0;
}
double work(int opt, double v1, double v2)
{
switch(opt)
{
case '+':
return v1 + v2;
case '-':
return v1 - v2;
case '*':
return v1 * v2;
case '/':
return v1 / v2;
case '^':
return pow(v1,v2);
}
return 0;
}
void pop()
{
double tmp = work(stk_p[top_p], stk_val[top_val - 1], stk_val[top_val]);
top_val -= 2;
stk_val[++top_val] = tmp;
top_p --;
return;
}
int main()
{
scanf("%s", s);
n = strlen(s);
if(s[0] == '-') stk_val[++ top_val] = 0;
for(int i = 0; i < n; i ++)
{
if(s[i] == '.') continue;
if(s[i] >= '0' && s[i] <= '9')
{
double res = 0;
while(s[i + 1] >= '0' && s[i + 1] <= '9')
res = res * 10 + s[i] - '0', i++;
res = res * 10 + s[i] - '0';
if(s[i + 1] == '.' && s[i + 2] >= '0' && s[i + 2] <= '9')
{
double r = 0, p = 10;
i += 2;
while(s[i + 1] >= '0' && s[i + 1] <= '9')
r += (s[i] - '0') / p, p *= 10;
r += (s[i] - '0') / p;
res += r;
}
stk_val[++top_val] = res;
}
else if(s[i] == '(')
{
stk_p[++ top_p] = '(';
if(s[i + 1] == '-') stk_val[++ top_val] = 0;
}
else if(s[i] == ')')
{
while(top_p && stk_p[top_p] != '(') pop();
if(top_p == 0)
{
puts("括号不匹配,缺少左括号");
return 0;
}
top_p --;
}
else if(pr(s[i]) > 0)
{
while(top_p && pr(s[i]) <= pr(stk_p[top_p])) pop();
stk_p[++ top_p] = s[i];
}
}
while(top_p && pr(stk_p[top_p])) pop();
if(top_p) puts("括号不匹配, 缺少右括号");
else printf("%.6lf\n", stk_val[1]);
return 0;
}
此代码已通过洛谷 \(\text{P10473 表达式计算4}\) 并利用下述数据进行测试.
$$
\begin{array} {|c|c|}\hline
\text{输入} & \text{输出} \\hline
2^{\wedge}10-1000-(5^{\wedge} 3)+(3^{\wedge}2) & -92 \\hline
1-2+3-4+5-6 & -3 \\hline
-(1-2^\wedge 3)^\wedge(-2)+1 & 0.979592 \\hline
2^\wedge 32/4^\wedge 2 & 1 \\hline
2(1+2^\wedge 2)/(32-3)(1^\wedge 10+2)-3*2 & 4 \\hline
((1+2) & \text{缺少左括号} \\hline
1+2) & \text{缺少右括号} \\hline
\end{array}
$$
股票走势
\subsubsection{朴实的设计思想}
我们直接枚举一个右端点 \(i\), 然后从 \(i\) 开始往左枚举 \(j\), 直到遇到第一个比当前值大的位置. 时间复杂度 \(\text O(n^2)\).
\subsubsection{利用栈}
考虑维护一个单调栈, 此处我们需要找到左边第一个比自己大的位置, 那么就从左向右维护一个单调递减的单调栈.
具体过程就是, 从左至右遍历数组, 如果当前值比栈顶大, 就将栈顶弹出, 重复此过程. 那么栈顶元素就是左边最近的比当前值大的元素. 如果栈是空的, 就说明当前值左边没有比它大的, 即 \(s_i = i\)(当下标从 \(1\) 开始). 在弹出之后, 将当前值入栈.
利用数学归纳法的思想不难证明, 按照上述操作维护的栈是单调的.
时间复杂度 \(\text O(n)\).
具体代码如下.
const int N=2e5+10;
int stk[N], top;
int n, a[N], s[N];
int main()
{
read(n);
for(int i = 1; i <= n; i ++)
{
read(a[i]);
while(top && a[i] >= a[stk[top]]) top--;
s[i] = i - stk[top];
stk[++top] = i;
}
for(int i = 1; i <= n; i ++)
write(s[i],i < n ? ' ' : '\n');
flushout();
return 0;
}
Task 4: 基数排序
设计分析
基数排序, 就是按数位进行排序, 而越高的数位, 其对排序的影响更大. 我们考虑从低位到高位依次排序.
对于每一位, 将所有数按照这一位一次加入到编号为 \(0\sim 9\) 的十个队列中. 并按编号依次将这十个队列拼接, 每一位就基于上一位拼接的队列顺序进行分组.
而对于字符串同理, 只需将 \(0\sim 9\) 改为 \(a\sim z\) 用 26 个队列分组即可.
复杂度分析
在基数排序中, 我们可以认为整数就是由 \(0\sim 9\) 构成的字符串. 所以在将前导 \(0\) 补全后, 其复杂度与字符串一致.
记 \(m\) 为数据的位数, \(n\) 为待排序的数据个数. 则时间复杂度为 \(\text O(mn)\).
代码实现
\subsubsection{整数}
int main()
{
freopen("radixSort1.txt","r",stdin);
for(int v;scanf("%d",&v)!=EOF;)
a.push(v);
n = a.size();
for(int p = 1, fg = 1; fg;p *= 10)
{
while(!a.empty())
{
int now = a.front();
a.pop();
q[now/p%10].push(now);
}
if(q[0].size() == n) fg = 0;
for(int i = 0; i < 10; i ++)
while(!q[i].empty())
a.push(q[i].front()), q[i].pop();
}
while(!a.empty())
res.push_back(a.front()),
a.pop();
return 0;
}
\subsubsection{字符串}
为了操作简单, 直接使用 \(\text{ASCII}\) 码进行字符映射, 所以 \(q\) 的编号范围在 \(0\sim 127\) 但实际使用部分只有 52 个.
int main()
{
for(;scanf("%s",s[++ n])!=EOF;)
a.push(n);
n --;
for(int p = 7;p >= 0; p--)
{
while(!a.empty())
{
int now = a.front();
a.pop();
q[s[now][p]].push(now);
}
for(int i = 0; i < 128; i ++)
while(!q[i].empty())
a.push(q[i].front()), q[i].pop();
}
while(!a.empty())
res.push_back(a.front()),
a.pop();
return 0;
}
评论