字符串和链表--数据结构与算法--学习指南

<数据结构与算法>学习笔记(一)基础知识-基本数据结构

主要参考:《数据结构与算法/leetcode/lintcode题解》《胡伟煌 数据结构 学习笔记》

1.String(字符串)

总结一些在C++、Java、Python中对于字符串的一些常见操作。

1.1 python

首先是Python。先看一下Python中内置的字符串类型的方法都有哪些(带下划线的我们通常不用的所以过滤掉了):

In[19]: ls=[x for x in str.__dict__ if not x.endswith("_") ];
In[19]: ls.sort()
In[19]: print(ls)
Out[19]: ['capitalize', 'casefold', 'center', 'count', 'encode', 'endswith', 'expandtabs', 'find', 'format', 'format_map', 'index', 'isalnum', 'isalpha', 'isascii', 'isdecimal', 'isdigit', 'isidentifier', 'islower', 'isnumeric', 'isprintable', 'isspace', 'istitle', 'isupper', 'join', 'ljust', 'lower', 'lstrip', 'maketrans', 'partition', 'replace', 'rfind', 'rindex', 'rjust', 'rpartition', 'rsplit', 'rstrip', 'split', 'splitlines', 'startswith', 'strip', 'swapcase', 'title', 'translate', 'upper', 'zfill']

更详细的信息可以通过help(str)来获取。

主要的函数有:

  • capitalize(): -Return a capitalized version of the string-返回首字母大写的版本
  • casefold(): - Return a version of the string suitable for caseless comparisons-我的理解是返回字符串的小写版本
  • center(width,fillchar=’ '): - Return a centered string of length width. Padding is done using the specified fill character (default is a space).-返回的字符串的长度为第一个参数,第二个参数默认为空格,第二个参数为一个字符的话返回的字符串用该字符填充
  • count(…): 返回某个字符串出现的次数
  • encode(encoding=‘utf-8’, errors=‘strict’): -Encode the string using the codec registered for encoding.-使用已注册的编码解码器编码字符串
  • endswith( …): 返回布尔值表示某个字符是否为该字符串的尾字符
  • expandtabs(tabsize=8): -Return a copy where all tab characters are expanded using spaces. If tabsize is not given, a tab size of 8 characters is assumed.-返回将原字符串中的制表符替换成默认8字节的空格的字符串
  • find(…): 返回某个字符在字符串中首次出现的位置(下标),不在字符串中出现则返回-1
  • index(…): 返回某个字符在字符串中首次出现的位置(下标),不在字符串中出现则报错
  • isalnum(): 如果字符串中值包含字母和数字则返回true,否则返回false
  • isalpha(): 如果字符串中只包含字母则返回true,否则返回false
  • isascii(): 如果字符串是ASCII码则返回true,否则返回false
  • isdigit(): - Return True if the string is a digit string, False otherwise.-如果字符串只包含数字则返回true,否则返回false
  • isidentifier(): 如果字符串是有效的标识符返回true,否则返回false
  • islower() :如果字符串只包含小写字母返回true,否则返回false
  • isnumeric(): -Return True if the string is a numeric string, False otherwise.-如果字符串是一个数值型字符串则返回true,否则返回false
  • isprintable(): 字符串是可打印的则返回true,否则返回false
  • isspace(): 如果字符串是空白字符则返回true,否则返回false
  • istitle(): 如果字符串是首字母大写后面都是小写字母这种形式则返回true,否则返回false
  • isupper(): 如果字符串中全是大写字母则返回true,否则返回false
  • join(iterable): 将字符串拼接起来。例如:'.'.join(['ab', 'pq', 'rs']) -> 'ab.pq.rs'
  • ljust(width,fillchar=’ '): 类似于center,但它是左对齐
  • lower(): 返回原字符串的小写版本
  • lstrip(chars=None): 不传入参数的话是将原字符串中如果前缀是空白字符则移除然后返回,如果一个字符的话就是去掉字符串的特定前缀并返回
  • partition(sep):传入一个参数,首先它会找到字符串中首次出现该字符的位置,然后返回这个字符位置前部分的字符串、这个字符、后部分的字符串这三个字符串组成的元组。没有找到该字符则返回该字符串和两个空白字符的三元组。
  • replace(old,new,count=-1): 字符替换,count默认-1表示字符串中所有的old字符替换成new字符,count给定一个数值字符串替换重复多少次
  • rfind(…): 返回某个字符在字符串中最后一次出现的位置,没有则返回-1
  • rindex(…):返回某个字符在字符串中最后一次出现的位置,没有则报错
  • rjust(width,fillchar=’ '): 类似于center,它是右对齐
  • rpartition(sep): 分割,类似于partition,但它是从右往左的
  • split(sep=None,maxsplit=-1): 将seq作为分割符分割字符串并返回得到的所有子字符串的列表,maxsplit为分割次数,默认-1为完全分割。
  • rsplit(sep=None,maxsplit=-1): 类似split,但它是从右往左
  • rstrip(sep=None): 类似于lstrip,但它是去除尾部的空白字符或者指定字符
  • splitlines( keepends=False):以换行符为分割符来分割字符串并返回对应列表
  • startwith(…): 判断首字符是否是指定字符
  • strip(chars=None): 类似于lstrip,但它是同时删除首部或尾部的空白符或者指定字符
  • swapcase(): 将大写字符变成小写同时将小写变成大写
  • title(): 将字符串变成title形式的
  • translate(table): 用给定的翻译表替换字符串中的字符
  • upper(): 将字符串中的字母大写并返回
  • zfill(width): 左边填充字符'0'使字符串达到指定长度并返回

基本上就是这些,用法都非常简单的,都是很基本的字符串操作。

1.2 Java

然后是 Java。对于Java,要了解它的一些基本类型的常见用法,最直接的方法是去找它的官方文档。https://docs.oracle.com/en/java/javase/14/docs/api/index.html。

一些字符串相关的类型:StringStringBufferStringBuilder都定义在java.lang模块中,String是基本的字符串类型,StringBuffer是线程安全的长度可变的字符串,而StringBuilder不是线程安全的,但通常单线程下效率更高,所以一般用StringBuilder。

相关的构造函数和方法官网中列出了很多。

示例如下

package algo_study;

public class string {
    public static void main(String[] args) {
        String str="Hello world";
        String str1=new String(new char[]{'a','b','c'});
        String str2=new String(new char[]{'a','b','c','d'},1,3);
        String str3=new String("Hello world");
        String str4=new String(new StringBuffer("stringbuffer"));
        String str5=new String(new StringBuilder("stringbuilder"));
        System.out.println("str--"+str);
        System.out.println("str2--"+str1);
        System.out.println("str3--"+str2);
        System.out.println("str4--"+str3);
        System.out.println("str5--"+str4);
        System.out.println("str6--"+str5);
        System.out.println(str==str3);//false
        System.out.println(str.equals(str3));//true
        char ch=str.charAt(1);
        int ind=str.indexOf('l');
        //StringBuilder
        StringBuilder sbui1=new StringBuilder();
        StringBuilder sbui2=new StringBuilder(10);
        StringBuilder sbui3=new StringBuilder(str);
        System.out.println("sbui1--"+sbui1);
        System.out.println("sbui2--"+sbui2);
        System.out.println("sbui3--"+sbui3);
        sbui3.append(str);
        sbui3.append(sbui3);
        System.out.println("sbui3--"+sbui3);
        sbui1=sbui3.delete(4,8);
        System.out.println("sbui1--"+sbui1);
        sbui2=sbui3.deleteCharAt(5);
        System.out.println("sbui2--"+sbui2);
        System.out.println(sbui3.insert(0,"test"));
        System.out.println(sbui3.reverse());
        System.out.println(sbui3.replace(0,2,"test"));
        sbui3.setCharAt(0,'x');
        System.out.println(sbui3);
        System.out.println(sbui3.substring(3,6));
        System.out.println(sbui3.subSequence(4,7));
    }
}

输出

str--Hello world
str2--abc
str3--bcd
str4--Hello world
str5--stringbuffer
str6--stringbuilder
false
true
sbui1--
sbui2--
sbui3--Hello world
sbui3--Hello worldHello worldHello worldHello world
sbui1--HellrldHello worldHello worldHello world
sbui2--HellrdHello worldHello worldHello world
testHellrdHello worldHello worldHello world
dlrow olleHdlrow olleHdlrow olleHdrlleHtset
testrow olleHdlrow olleHdlrow olleHdrlleHtset
xestrow olleHdlrow olleHdlrow olleHdrlleHtset
tro
row

进程已结束,退出代码0

1.3 C++

C++中的字符串string作为一种顺序容器,随机访问块,尾部插入、删除块。很多顺序容器能用的操作string 也能用。完全可以把string理解成一个储存char类型元素的一个vector容器,当然其中有些细节是需要注意的。

相比较而言,C++中的字符串操作会稍微复杂一些,也容易出错,但还是挺好用的,而且执行效率高。主要的一些操作如下

#include <iostream>
#include<string>

int main()
{
	std::string str;
	std::string str1{ 'a','b','c' };
	std::string str2(str1);
	std::string str3(str1.begin()+1, str1.end());
	std::cout << "size of str1: " << str1.size() << std::endl;
	str3.swap(str1);//拷贝
	std::cout << str3 << std::endl;
	str2 = "test";
	str3.assign(str2);//拷贝
	std::cout << str3 << std::endl;
	str1.assign(str2.begin(), str2.end());
	std::cout << str1 << std::endl;
	str1.push_back('t');//尾部添加一个字符
	std::cout << str1 << std::endl;
	str1.insert(str1.begin(), 5, 'x');//插入字符
	std::cout << str1 << std::endl;
	str1.insert(str1.begin(), str2.begin(), str2.end());
	std::cout << str1 << std::endl;
	str1.insert(str1.begin(), { 't','e','s','t' });
	std::cout << str1 << std::endl;
	std::cout << str1.at(0) << std::endl;
	std::cout << str1[0] << std::endl;
	str1.pop_back();//返回尾部字符并删除
	std::cout << str1 << std::endl;
	str1.erase(str1.begin());//删除迭代器指向的元素
	std::cout << str1 << std::endl;
	str1.erase(str1.begin(), str1.begin() + 3);
	std::cout << str1 << std::endl;
	str1.clear();//删除所有的元素
	std::cout << str1 << std::endl;

	std::string str4({'t','e','s','t'}, 2);
	std::string str5("The world", 4);
	std::string str6(str5, 2, 6);
	std::cout << str4 << std::endl;
	std::cout << str5 << std::endl;
	std::cout << str6 << std::endl;

	str2 = "hello world";
	str1 = str2.substr(0, 6);
	std::cout << str1 << std::endl;
	str1.append(str2);
	std::cout << str1 << std::endl;
	str1.replace(8, 3, "test");
	std::cout << str1 << std::endl;
	auto ind1 = str1.find('t', 1);//搜索
	auto ind2 = str1.rfind('t', 1);
	auto ind3 = str1.find(str2,1);
	auto ind4 = str1.find_first_of(str2, 1);
	auto ind5 = str1.find_first_not_of(str2, 1);
	auto ind6 = str1.find_last_of(str2, 1);
	auto ind7 = str1.find_first_not_of(str2, 1);
	std::cout << ind1 << " -" << ind2 << " -" << ind3 << " -"
		<< ind4 << " -" << ind5 << " -" << ind6 << " -" << ind7 << " -" << std::endl;

	int i = 12345;
	std::string s1 = std::to_string(i);
	double d = std::stod(s1);
	std::string s2 = "pi=3.141592654";
	d = stod(s2.substr(s2.find_first_of("+-.0123456789")));
	std::cout << "d=" << std::endl;

	return 0;
}

输出

size of str1: 3
abc
test
test
testt
xxxxxtestt
testxxxxxtestt
testtestxxxxxtestt
t
t
testtestxxxxxtest
esttestxxxxxtest
testxxxxxtest

st
The
e
hello
hello hello world
hello hetest world
8 -4294967295 -4294967295 -1 -8 -1 -8 -
d=3.14159

C:\Users\xhh\Source\Repos\algo_study1\Debug\algo_study1.exe (进程 9680)已退出,代码为 0。
按任意键关闭此窗口. . .

2. Linked List(链表)

基本概念:

首先是四种基本的存储结构:顺序存储、链式存储、索引存储、散列存储。

  • 顺序存储:把逻辑上相邻的元素存储在一组连续的存储单元中,元素之间的逻辑关系由存储单元地址间的关系隐含表示。这种结构的优点是节省存储空间,可以随机读取,缺点是不便于插入和删除某个节点。数组就是一种典型的顺序存储结构。
  • 链式存储:给每个节点增加指针字段,用于存放临近节点的存储地址。优点是便于修改。缺点是占用存储空间,且不支持随机访问。
  • 索引存储:存储节点的同时增加索引表,索引表的索引项是(关键字,地址),关键字表示节点,地址为节点的指针。各节点的地址在索引表中依次排列。优点是可以快速查找,随机访问,方便修改。缺点是增加事件和空间的开销。
  • 散列存储:根据节点的值确定节点的存储地址。以节点作为自变量,通过散列函数计算出结果然后把它作为节点的存储地址。优点是查找速度块,缺点是只存储节点,不存储节点之间的关系。

线性表是由n个节点组成的有限序列,满足特征:1)每个节点之多只有一个前驱节点且至多只有一个后继节点;2)起始节点没有前驱节点;3)终结节点没有后继节点。

线性表支持的基本运算有:1)初始化线性表;2)求线性表的长度;3)求线性表的第i个元素;4)按值查找元素,返回元素序号;5)插入元素;6)删除元素;7)输出列表。

线性表有两种存储方式:顺粗存储和链式存储。

而链表就是链式存储的线性表。根据指针域的不同,链表可以分为单向链表、双向链表、循环链表等。

要想理解清楚链表的程序实现,我觉得做好还是先在C++或者C语言上实现,因为很多语言是没有指针的概念的,这样操作起来感觉会容易造成思维混乱,而C/C++上实现就比较思路清晰。而且感觉其它语言像python或者Java都是经过封装的,大多数时候都有现成的直接用。

1. C++

首先是单链表。单链表的一个节点定义如下:

//单链表的一个节点包含一个变量和指向下一个节点的指针
struct listNode
{
	int val;
	listNode* next;
	listNode(int v, listNode* n = nullptr) :val(v), next(n) {}
};

难点是将一些常见的对链表的操作包装到一个类中,一些基本的概念都理解的前提下,要实现这些基本操作还是需要费一番功夫的。因为指针本身还是很抽象的概念,即使完全理解了它的作用机制,但要利用指针来实现一些简单操作至少对于初学者来说是很有难度的,必须时刻注意一个指向某种类型的指针的变量,我们什么时候需要用到这个指针变量指向的元素,什么时候需要它的地址(指针本质上就是一个地址,通过赋值改变它的地址也就改变了它指向的元素)。

下面是我个人完成的一个类的定义,实现了前面列出的7个基本运算。这个类可以作为一个容器。

#include"ListNode.cpp"
#include <iostream>


class linkedList {
	private:
		listNode* head;//头部,注意,头部不用来储存数据,只是作为起始点
		int length;//长度
	public:
		//friend std::ostream& operator<<(std::ostream& out, linkedList& ll);

		linkedList() :head(new listNode(0)), length (0){}	//构造函数,初始化
		~linkedList() { delete head; }						//析构函数
		int size() { return this->length; }					//求长度
		const int operator[](int n) const{//求			//求链表中的某个值
			if (n < length) {
				listNode* pNode = head;
				for (int i = 0; i <= n; ++i)
					pNode = pNode->next;
				return pNode->val;
			}
			else
				return 0;
				std::cerr << "error:subscript overflow."<<std::endl;
		}
		size_t find(int i) {								//按值查找元素并返回下标
			listNode* pNode = head->next;
			int ind = 0;
			while (pNode != nullptr) {
				if (i == pNode->val)
					return ind;
				else {
					pNode = pNode->next;
					ind++;
				}
			}
			return -1;//没找到
		}
		void insert(int ind,int n) {							//插入一个值
			listNode* ins = new listNode(n);
			if (ind <= length &&ind>=0 ) {
				listNode* pNode;
				pNode = head;
				for (int i = 0; i < ind; ++i)
					pNode = pNode->next;
				ins->next = pNode->next;
				pNode->next = ins;
				length++;
			}
			else if (ind == -1) {
				listNode* p ;
				p = head;
				while (p->next != nullptr)
					p = p->next;
				p->next = ins;
				this->length ++;
				//std::cout << length << std::endl;
			}
			else {
				std::cerr << "error: invalid subscript,insert data failed"<<std::endl;
			}
		}
		void pushback(int n) {								//添加到链表末尾
			insert(-1, n);
		}
		void erase(int ind) {							//删除元素
			if (ind < length && ind >= 0) {
				listNode* pNode = head;
				for (int i = 0; i < ind; ++i)
					pNode = pNode->next;
				pNode->next = pNode->next->next;
				this->length--;
			}
			else
				std::cerr << "error:invalid subscript,erase data failed!" << std::endl;;
		}
		listNode* HEAD(){
			return head;
		}
		void printThis() {								//输出整个链表中的数据
			listNode* p = head;
			while(p->next!=nullptr) {
				p = p->next;
				std::cout << p->val<<", ";
			}
			std::cout << std::endl;
		}
		void reverse() {						//反转链表
			listNode* pro, * pn = nullptr,*temp;
			pro =  head->next;
			while (pro!= nullptr) {
				temp = pro->next;
				pro->next = pn;
				pn = pro;
				pro = temp;	
			}
			head->next = pn;
		}
};

然后是使用这个类来具体的实现我们需要的操作:

#include <iostream>
#include<string>
#include"linkedList.cpp"

std::ostream& operator<<(std::ostream& out, linkedList& ll) {       //输出整个链表,注意双目运算符必须在类之外定义
	listNode* pNode;
	pNode = ll.HEAD();
	while (pNode->next != nullptr) {
		pNode = pNode->next;
		out << pNode->val << ", ";
	}
	return out;
}
int main()
{
	linkedList ll;
	std::cout << "链表的长度:"<<ll.size() << std::endl;
	for (int i = 0; i < 10; ++i)
		ll.pushback(i*i);
	std::cout << "链表的长度:" << ll.size() << std::endl;
	std::cout << "ll[3]=" << ll[3] << std::endl;
	std::cout << "查找 16,下标:" << ll.find(16) << std::endl;
	std::cout << "ll的所有元素:" <<  std::endl;
	ll.printThis();
	ll.insert(0, 1111);
	std::cout << "ll的所有元素:" << std::endl;
	ll.printThis();
	ll.erase(1);
	ll.printThis();
	ll.reverse();
	ll.printThis();
	std::cout << ll << std::endl;
	return 0;
}

输出

链表的长度:0
链表的长度:10
ll[3]=9
查找 16,下标:4
ll的所有元素:
0, 1, 4, 9, 16, 25, 36, 49, 64, 81,
ll的所有元素:
1111, 0, 1, 4, 9, 16, 25, 36, 49, 64, 81,
1111, 1, 4, 9, 16, 25, 36, 49, 64, 81,
81, 64, 49, 36, 25, 16, 9, 4, 1, 1111,
81, 64, 49, 36, 25, 16, 9, 4, 1, 1111,

C:\Users\xhh\Source\Repos\algo_study1\Debug\algo_study1.exe (进程 14848)已退出,代码为 0。
按任意键关闭此窗口. . .

用C++来实现链表还是很有意义的。另外也可以尝试用C语言来实现链表,不过感觉和C++的代码会比较类似的。

2. Java

同样的思路用Java写就简单了很多,自己实际操作起来就可以明显感受到C++和Java之间的一些差别,用C++的时候还debug了好久,当然可能一开始还在构思其中的逻辑,但用Java写直接能正常运行都没有出现Bug还是让我有点意外的。虽然二者的总的代码行数还是差不多的,但和C++相比,Java仿佛有一种简洁的美感,就很神奇。注意到,Java舍弃了C++中的可以操作符重载的特性,这样提高了可读性,也更不容易出错了,这一点是很不错的。前面我用C++实现链表的时候很长的事件都在纠结操作符重载的问题。

然而,虽然Java没有指针的概念,但似乎并不影响它去实现链表,反而使这个过程更加的简便。表面是它使没有指针的概念的,但它的底层肯定使需要借助相关的概念来实现。我们理解上是要知道其中有指针的操作。

首先是定义节点

package algo_study;

public class listNode {
    public int val;
    public listNode next;
    public listNode(int val){
        this.val=val;
        this.next=null;
    }
}

然后我把链表的类的定义和具体使用放一起了。

package algo_study;

public class linkedList {
    private listNode head;
    private int length;
    public linkedList(){
        this.head=new listNode(0);
        this.length=0;
    }
    public int getLength() {        //获取链表长度
        return length;
    }
    public int at(int ind){         //返回链表中的第几个值,Java中并不支持运算符重载,没法像C++那样玩花的~
        listNode ln=head;
        if(ind >= 0 && ind < length){
            for(int i=0;i<=ind;++i)
                ln=ln.next;
            return ln.val;
        }else if(ind==-1){
            while(ln.next!=null)
                ln=ln.next;
            return ln.val;
        }else{
            System.out.println("error subscript");
            return 0;
        }
    }
    public int find(int i){             //查找某个值并返回下标
        listNode ln=head.next;
        int ind=0;
        while(ln!=null){
            if(i==ln.val)
                return ind;
            else{
                ln=ln.next;
                ind++;
            }
        }
        return -1;
    }
    public void insert(int ind,int n){          //插入数据
        listNode ln=new listNode(n);
        if(ind<=length&&ind>=0){
            listNode ln1=head;
            for(int i=0;i<ind;++i)
                ln1=ln1.next;
            ln.next=ln1.next;
            ln1.next=ln;
            length++;
        }else if(ind==-1){
            listNode p=head;
            while(p.next!=null)
                p=p.next;
            p.next=ln;
            length++;
        }else
            System.out.println("error:invalid subscript, insert data fained");
    }
    public void pushBack(int n){
        insert(-1,n);
    }
    public void erase(int ind){             //删除数据
        if(ind<length &&ind>=0){
            listNode pNode=head;
            for(int i=0;i<ind;++i)
                pNode=pNode.next;
            pNode.next=pNode.next.next;
            this.length--;
        }else if(ind==-1){
            listNode pNode=head;
            while(pNode.next.next!=null)
                pNode=pNode.next;
            pNode.next=null;
            length--;
        }else
            System.out.println("error:erase date failed");
    }
    @Override
    public String toString() {          //重载这个方法方便打印链表的所有数据
        StringBuilder str=new StringBuilder("[");
        listNode ln=head;
        while(ln.next!=null){
            ln=ln.next;
            str.append(" "+ln.val);
        }
        str.append(" ]");
        return str.toString();
    }
    public void reverse(){          //反转链表
        listNode pro,pn=null,temp;
        pro=head.next;
        while(pro!=null){
            temp=pro.next;
            pro.next=pn;
            pn=pro;
            pro=temp;
        }
        head.next=pn;
    }

    public static void main(String[] args) {
        linkedList LL=new linkedList();
        System.out.println(LL.getLength());
        for(int i=0;i<10;++i)
            LL.pushBack(i*i);
        System.out.println(LL.getLength());
        System.out.println("LL: "+LL);
        LL.insert(1,1234);
        System.out.println(LL);
        LL.erase(2);
        System.out.println(LL);
        LL.reverse();
        System.out.println(LL);
    }
}

输出

0
10
LL: [ 0 1 4 9 16 25 36 49 64 81 ]
[ 0 1234 1 4 9 16 25 36 49 64 81 ]
[ 0 1234 4 9 16 25 36 49 64 81 ]
[ 81 64 49 36 25 16 9 4 1234 0 ]

进程已结束,退出代码0

3. python

python用起来又是完全不同的感觉了,整个过程是,先考虑了半天完成C++版本的,然后照着C++的版本用Java写,最后照着Java的版本来写python。由于是照着写的,没有多加考虑,也没有充分发挥python代码简洁的特性。过程中基本也没有遇到什么困难。python的一些高级特性比如装饰器我一直没有去认真研究,有些细节甚至可以自己猜到,比如这里的怎么让内置的print函数打印我们定义类输出我们想要的结果,在没有特意查资料且以前没遇到这个问题的情况下,我直接猜到了是在类中定义__str__函数,并让它返回字符串。

整体下来,感觉还是Java的使用体验最好。C++优点太复杂了,python又比较松散不严谨,而Java就是恰到好处的。

首先是节点定义:

class listNode:
    def __init__(self,v):
        self.val=v
        self.next=None

然后是链表的实现

from algo_study import listNode

class linkedList:
    def __init__(self):
        self.head=listNode.listNode(0)
        self.length=0
    def size(self):
        return self.length
    def at(self,ind):
        ln=self.head
        if ind>=0 and ind<self.length:
            for x in range(ind+1):
                ln=ln.next
            return ln.val
        elif ind==-1:
            while ln.next!=None:
                ln=ln.next
            return ln.val
        else:
            print("Error")
    def find(self,i):
        ln=self.head.next
        ind=0
        while ln!=None:
            if(i==ln.val):
                return ind
            else:
                ln=ln.next
                ind+=1
        return -1
    def insert(self,ind,n):
        ln=listNode.listNode(n)
        if ind<=self.length and ind>=0:
            ln1=self.head
            for i in range(ind):
                ln1=ln1.next
            ln.next=ln1.next
            ln1.next=ln
            self.length+=1
        elif ind==-1:
            p=self.head
            while(p.next!=None):
                p=p.next
            p.next=ln
            self.length+=1
        else:
            print("error")
    def pushBack(self,n):
        self.insert(-1,n)
    def erase(self,ind):
        if ind in range(self.length):
            p=self.head
            for i in range(ind):
                p=p.next
            p.next=p.next.next
            self.length-=1
        elif ind ==-1:
            p=self.head
            while p.next.next!=None:
                p=p.next
            p.next=None
            self.length-=1
        else:
            print("error")
    def toString(self):
        str1,p="[",self.head
        while p.next!=None:
            p=p.next
            str1+=" "+str(p.val)
        str1+=" ]"
        return str1
    def reverse(self):
        pro,pn,temp=self.head.next,None,None
        while(pro!=None):
            temp=pro.next
            pro.next=pn
            pn=pro
            pro=temp
        self.head.next=pn
    def __str__(self):
        return self.toString()
LL=linkedList()
print(LL.size())
for x in range(10):
    LL.pushBack(x*x)
print(LL.size())
print(LL.toString())
LL.insert(1,1234)
print(LL)
LL.erase(2)
print(LL)
LL.reverse()
print(LL)

输出

0
10
[ 0 1 4 9 16 25 36 49 64 81 ]
[ 0 1234 1 4 9 16 25 36 49 64 81 ]
[ 0 1234 4 9 16 25 36 49 64 81 ]
[ 81 64 49 36 25 16 9 4 1234 0 ]

3. 双向链表

双向链表的话对于每个节点都有两个指针prevnext,分别指向前一个节点和后一个节点,头部的prev为空指针,尾部的next为空指针,相比较单向链表,它可以更快速的操作双端的元素。理解了单向链表的话双向链表并没有什么难的。

C++版本

虽然能理解个大概,但具体的细节要把握准确还是有一定的难度的。需要很灵活的处理实际遇到的情况。

像这里我们要实现双向链表的一些操作,那么包括查找、插入、删除、反转等操作和单链表都是很不一样的,这时也是很容易出错的。

首先是双向链表的节点类定义,我现在才发现Visual Stdio 2019新建一个C++类时它会自动生成对应的头文件和cpp源文件,当然同时用到头文件和源文件来定义一个类才是最规范的定义类的格式,但通常我们定义一个简单的类是是直接在源文件中定义的,都不会用到头文件,实际上头文件里的语法也是按照C++语法规范的,但这种头文件加源文件的定义一个类的方式应该是官方所推荐的。试了一下,这种方式确实很不错,尤其是对于一些大型项目,这种方式还是很有用的。

dListNode.h

#pragma once
class dListNode
{
public:
	int val;
	dListNode* prev, * next;
	dListNode(int v);

};

dListNode.cpp

#include "dListNode.h"

dListNode::dListNode(int v):val(v), prev(nullptr), next(nullptr) {}

然后是双向链表的实现

dLinkedList.h

#pragma once
#include "dListNode.h"

class dLinkedList
{
private:
	dListNode* head, *tail;		//头部和尾部,不储存具体数值,仅作为标志
	int length;
public:
	dLinkedList();
	~dLinkedList();
	int size();//大小
	const int operator[](int n)const;//at
	int find(int i);//搜素
	void insert(int ind,int n);//插入
	void pushback(int n);//末尾
	void add(int n);//添加到头部
	void erase(int ind);//删除
	int pop();//弹出尾部的元素
	int popHead();//弹出首个的元素
	void printThis();//打印所有的元素
	void reverse();//反转链表

};

dLinkedList.cpp

#include "dLinkedList.h"
#include<iostream>
//构造函数,注意初始时要求head的next指向tail,tail的prev指向head
dLinkedList::dLinkedList() :head(new dListNode(0)), tail(new dListNode(0)), length(0) { head->next = tail; tail->prev = head; }
dLinkedList::~dLinkedList() { delete head, delete tail; }
int dLinkedList::size() { return length; }

const int dLinkedList::operator[](int n) const
{
	dListNode* p1 = head,*p2=tail;
	if (n < 0)
		n += length;		//允许下标去负数,如-1->length-1
	if (n < length && n >= 0) {
		if (n <= length / 2) {				//判断我们访问的元素是靠近头部还是靠近尾部,分别采取两个方向来接近该元素,
			for (int i = 0; i <= n; ++i)	//这样可以充分利用双向链表的特性加快访问速度
				p1 = p1->next;
			return p1->val;
		}
		else {
			for (int i = length; i > n; --i)
				p2 = p2->prev;
			return p2->val;
		}
	}
	return 0;
}

int dLinkedList::find(int i)
{
	dListNode* p = head->next;
	int ind = 0;
	while (p ->next!= nullptr) {
		if (i == p->val)
			return ind;
		else {
			p = p->next;
			ind++;
		}
	}
	return -1;
}

void dLinkedList::insert(int ind, int n)
{
	dListNode* p1 = head, * p2 = tail,*pv=new dListNode(n);
	if (ind < 0)
		ind += length;
	if (length == 0 && ind == -1)
		ind = 0;
	if (ind <= length && ind >= 0) {
		if (ind <= length / 2) {
			for (int i = 0; i < ind; ++i)
				p1 = p1->next;
			pv->next = p1->next;//从中间插入一个值,需要修改四个指针的指向,这里一定要注意
			pv->prev = p1;
			p1->next->prev = pv;
			p1->next = pv;
		}
		else {
			for (int i = length-1; i > ind; i--)
				p2 = p2->prev;
			pv->next = p2;
			pv->prev = p2->prev;
			p2->prev->next = pv;
			p2->prev = pv;
		}
		length++;
	}
	else
		std::cerr << "insert failed!" << std::endl;
}

void dLinkedList::pushback(int n)
{
	insert(-1, n);
}

void dLinkedList::add(int n)
{
	insert(0, n);
}

void dLinkedList::erase(int ind)
{
	dListNode* p1 = head, * p2 = tail;
	if (ind < 0)
		ind += length;
	if (ind < length && ind >= 0) {
		if (ind <= length / 2) {
			for (int i = 0; i < ind; ++i)
				p1 = p1->next;
			p1->next->prev = nullptr;
			p1->next = p1->next->next;
			p1->next->prev = p1;
		}
		else {
			for (int i = length-1; i > ind; --i)
				p2 = p2->prev;
			p2->prev->next = nullptr;
			p2->prev = p2->prev->prev;
			p2->prev->next = p2;
		}
		length--;
	}
	else
		std::cerr << "erase failed!" << std::endl;
}

int dLinkedList::pop()
{
	int i = tail->prev->val;
	erase(-1);
	return i;
}

int dLinkedList::popHead()
{
	int i = head->next->val;
	erase(0);
	return i;
}

void dLinkedList::printThis()
{
	std::cout << "[";
	dListNode* p1 = head;
	while (p1->next->next != nullptr) {
		p1 = p1->next;
		std::cout << " " << p1->val;
	}
	std::cout << "]" << std::endl;
}

void dLinkedList::reverse()
{
	dListNode* p1 = head->next, * p2 = tail->prev, * temp = nullptr, * pn = tail;
	tail->prev = head->next;
	while (p1->next!= nullptr) {
		temp = p1->next;
		p1->next = pn;
		p1->prev = temp;
		p1 = temp;
		pn = temp->prev;
	}
	head->next = p2;
}

测试只需要在原来的main函数中稍微修改一下就可以了

#include <iostream>
#include<string>
#include"linkedList.cpp"
#include"dlinkedList.h"

int main()
{
	dLinkedList ll;
	std::cout << "链表的长度:"<<ll.size() << std::endl;
	for (int i = 0; i < 10; ++i)
		ll.pushback(i*i);
	std::cout << "链表的长度:" << ll.size() << std::endl;
	std::cout << "ll[3]=" << ll[3] << std::endl;
	std::cout << "查找 16,下标:" << ll.find(16) << std::endl;
	std::cout << "ll的所有元素:" <<  std::endl;
	ll.printThis();
	ll.insert(0, 1111);
	std::cout << "ll的所有元素:" << std::endl;
	ll.printThis();
	ll.erase(1);
	ll.printThis();
	ll.reverse();
	ll.printThis();
	//std::cout << ll << std::endl;
	return 0;
}

输出

链表的长度:0
链表的长度:10
ll[3]=9
查找 16,下标:4
ll的所有元素:
[ 1 4 0 9 16 25 36 49 64 81]
ll的所有元素:
[ 1111 1 4 0 9 16 25 36 49 64 81]
[ 1111 4 0 9 16 25 36 49 64 81]
[ 81 64 49 36 25 16 9 0 4 1111]

C:\Users\xhh\Source\Repos\algo_study1\Debug\algo_study1.exe (进程 50696)已退出,代码为 0。
按任意键关闭此窗口. . .
  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值