链表
链表是一个线性顺序结构,链表为动态集合提供了一种简单而灵活的表示方法,但由于指针的存在,在代码的书写过程中很容易出bug。不管是利用其他技巧模拟指针还是语言自带的指针都很容易出错。
下面给出链表的基本操作代码:の链表的结构图示就不给出了,网上搜索一大吧。
struct linkList{
node * head;
linkList(){
head = nullptr;
}
node * search(int val){
node * x = head;
while(x != nullptr && x->val != val){ //如果是循环链表还要判断是否已经循环了
x = x->next;
}
return x;
}
void insert(int val){
node * x = new node();
x->val = val;
if(head != nullptr)
head->prev = x;
x->next = head;
x->prev = nullptr;
head = x;
}
void listDelete(int v){
node * x = search(v);
listDelete(x);
}
void listDelete(node * x){
if(x->prev != nullptr)
x->prev->next = x->next;
else head = x->next;
if(x->next != nullptr)
x->next->prev = x->prev;
}
void out(){
node * x = head;
while(x !=nullptr){
printf("%d ", x->val);
x=x->next;
}
puts("");
}
};
就在写上面的示例代码的时候就出错了一次,还是简单的示例代码。并没有什么复杂的操作。便会有隐蔽的错误。
哨兵
哨兵是一个哑对象,其作用是简化边界条件的处理。如在循环双向链表中设置一个哨兵节点。且哨兵节点的prev和next互相指着。这样一个空的循环双向链表便存在了。对其的操作只需要将中间变量指针temp指向哨兵节点的next接下来只要判断temp是否指向哨兵节点便可判断是否遍历完该链表。
上面的链表实现并没有使用哨兵,使用哨兵节点可以简化代码。 但也仅限于简化代码。不能起到减小时间复杂度的作用(虽然在删除和插入的时候节省了判断head是否为空的操作);而且当需要大量的小链表来存储数据的时候,哨兵节点浪费的大量内存。下面给出使用哨兵的双向循环链表的代码:
struct linkListPlus{
node * head;
linkListPlus(){
head = new node();
head->prev = head;
head->next = head;
}
node * search(int val){
node * x = head->next;
while(x != head && x->val != val){
x = x->next;
}
return x;
}
void insert(int val){
node * x = new node();
x->val = val;
x->next = head->next;
head->next->prev = x;
head->next = x;
x->prev = head;
}
void listDelete(int v){
node * x = search(v);
listDelete(x);
}
void listDelete(node * x){
cout << x->val << endl;
x->prev->next = x->next;
x->next->prev = x->prev;
}
void out(){
node * x = head->next;
while(x !=head){
printf("%d ", x->val);
x=x->next;
}
puts("");
}
};
习题:
1. 单链表上的动态集合操作insert能否在O(1)时间内实现?delete操作呢?
答:不能,删除一个元素需要便利集合(顺序结构)找到该元素,然后改变前后指针的值达到删除的目的。将元素直接添加在链表头部确实可以做到O(1) 复杂度,但是一个集合是不允许拥有重复元素,这时需要遍历集合查重。最终两个操作在最坏情况下的时间复杂度为O(N);
2. 用一个单链表L实现一个栈。要求操作push和pop的运行时间仍为O(1).
答:代码如下:
struct listStack{
node * head;
listStack(){
head = new node();
head->prev = head->next = head;
}
int isEmpty(){
return head->next == head;
}
void push(int v){
node * x = new node();
x->next = head->next;
x->val = v;
head->next = x;
}
int pop(){
int x;
if(isEmpty())
return -1;// 发生溢出, 没有异常处理 所以返回-1代表异常
node * temp = head->next;
head->next = temp->next;
x = temp->val;
delete temp; //执行空间回收操作。避免内存泄漏
return x;
}
void out(){
printf("data: ");
node * x = head->next;
while(x !=head){
printf("%d ", x->val);
x=x->next;
}
puts("");
}
};
3. 用一个单链表L实现一个队列,要求操作enqueue和dequeue的运行时间仍为O(1).
答:代码如下:
struct listQueue{
node * front; // front->next 代表当前队首元素, front->prev代表队尾元素
listQueue(){
front = new node();
front->next = front->prev = front;
front->val = -1; // 哨兵节点front的值val可以做调用,调试的时候可给其他节点赋值为非-1的值 这样输出-1代表该执政指向哨兵节点
}
bool isEmpty(){
return front->next == front;
}
void enqueue(int v){
node * temp = new node();
temp->val = v;
temp->next = front->prev->next; //糊涂的用了错误代码:temp->next = front->prev;
front->prev->next = temp;
front->prev = temp;
}
int dequeue(){
if(isEmpty())
return -1;//溢出
int x;
node * temp = front->next;
front->next = temp->next;
x = temp->val;
delete temp;
return x;
}
void out(){
node * x = front->next;
while(x != front)
{
printf("%d ", x->val);
x=x->next;
}
puts("");
}
};
4. 在O(N)时间内反转一个链表,不能使用额外空间。
答:代码如下:交换每个节点prev和next指针的值即可。
void reverse(){
puts("reverse");
node * x = head, *next=head->next;
while(next != head){
next = x->next;
swap(x->next, x->prev);
x = next;
}
}
5. 实现只有一个指针的双向循环链表,题目提示使用整数值来表示指针,0表示空指针。这样即便有32为的整数,指针的范围也限制在一定范围内。先可一试:
struct onePointNode {
size_t p; //前16为代表prev, 后16位代表next 注意 前十六位代表高位十六位
int val;
const static int prev = 4294901760;
const static int next = 65535;
onePointNode() {
p = val = 0;
}
void setPrev(int x) {
p &= next; // 先将原来的prev部分
p |= (x<<16);
}
int getPrev() {
return (p >> 16) & next;
}
void setNext(int x) {
p &= prev; //清空旧的next部分
p |= x;
}
int getNext() {
return p & next;
}
};
struct onePointLinkList {
const static int N = 1000;
onePointNode dat[N];
stack<int> free; //充分利用空余空间 简单模拟的垃圾回收 重复利用机制
int head;
onePointLinkList() {
for(int i = N-1; i > 0; i--) {
free.push(i);
}
head = 0;
}
void insert( int v) {
int x = free.top();
free.pop();
dat[x].val = v;
dat[dat[head].getNext()].setPrev(x); //循环链表中哨兵节点的前驱指向空链表条件下插入的第一个元素
dat[x].setNext(dat[head].getNext());
dat[x].setPrev(head);
dat[head].setNext(x);
}
int search(int v) {
int x = dat[head].getNext();
while(x != 0 && dat[x].val != v) {
x = dat[x].getNext();
}
return x;
}
void nodeDelete(int v) {
int x = search(v);
if(x != 0) {
dat[dat[x].getNext()].setPrev(dat[x].getPrev());
dat[dat[x].getPrev()].setNext(dat[x].getNext());
free.push(x);
}
}
void out() {
int x = dat[head].getNext();
while(x != 0) {
printf("%d ", dat[x].val);
x = dat[x].getNext();
}
puts("");
}
};
在不支持显式指针数据类型的编程环境下实现链表
在不支持显式指针的环境下可以利用上面的int类型的位运算+数组的方式实现简单的但元素个数控制在一定范围内的链表。还可以用但数组模拟同构对象的链表和使用多数组而不是上面的结构体数组实现。不过都是利用其他技巧模拟指针达到目的。所以这里就不另加编程实现了。以后有机会补上。紧凑的模拟链表
上面给出的利用位运算实现的链表中有一个点:就是实现了简单的垃圾回收重复利用栈free。这个栈的结构使每次创建的节点都来至栈顶,这时删除的元素因为重新入栈而达到下一次的插入位置在上一次删除的位置使元素尽量的紧凑,这时在使用分页虚拟存储的计算机中能够提高一定的访问速度。