/***
* 将文件切割成片
* @param filename
* @param uuid
* @param data
* @throws IOException
*/
default void divideToSegments(String filename, String uuid, byte[]data) throws IOException { DivideTask divideTask = new DivideTask(filename,uuid,data); Future<ImmutablePair<PlayList, List<TransportSegment>>> divideFuture = getThreadPool().submit(divideTask); String mediaId = String.format("media.%s",uuid); try {
ImmutablePair<PlayList, List<TransportSegment>> plsAndTsFiles = divideFuture.get(30, TimeUnit.MINUTES);
PlayList playlist = plsAndTsFiles.getLeft();
List<TransportSegment> segments = plsAndTsFiles.getRight(); //保存切片文件
saveSegments(segments);
//保存播放列表
savePlayList(playlist); //放到缓存里
Map<String,String> mapping = new HashMap<>();
mapping.put("playlist",playlist.getContext());
//把原始文件放进去,方便以后下载
mapping.put("binary",Base64.getEncoder().encodeToString(Files.readAllBytes(Paths.get(filename)))); for (TransportSegment segment:segments)
{
String tsFileName = segment.getFilename();
byte[] bytes = segment.getBytes();
String binary = Base64.getEncoder().encodeToString(bytes);
mapping.put(tsFileName,binary);
} //切片以后的文件添加到缓存
getCacheService().setCacheMap(mediaId, mapping); //30分钟以后失效
getCacheService().expire(mediaId,7,TimeUnit.DAYS); } catch (InterruptedException| ExecutionException | TimeoutException e) {
getLogger().error("文件切片失败:{}",e);
}
}
import lombok.Data;
import lombok.NoArgsConstructor; import javax.persistence.Table;
import java.time.LocalDateTime;
import java.time.ZoneId; /***
* 转换后的文件切片
*/
@Data
@NoArgsConstructor
@Table(name = "open_segment")
public class TransportSegment
{
String uuid; /***
* 文件名
*/
private String filename; /***
* 字节流
*/
private byte[] bytes; private LocalDateTime createTime = LocalDateTime.now(ZoneId.of("+8")); private TransportSegment(Builder builder) {
setUuid(builder.uuid);
setFilename(builder.filename);
setBytes(builder.bytes);
setCreateTime(builder.createTime);
} public static final class Builder {
private String uuid;
private String filename;
private byte[] bytes;
private LocalDateTime createTime = LocalDateTime.now(ZoneId.of("+8")); public Builder() {
} public Builder uuid(String uuid) {
this.uuid = uuid;
return this;
} public Builder filename(String filename) {
this.filename = filename;
return this;
} public Builder bytes(byte[] bytes) {
this.bytes = bytes;
return this;
} public Builder createTime(LocalDateTime createTime) {
this.createTime = createTime;
return this;
} public TransportSegment build() {
return new TransportSegment(this);
}
}
}
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDateTime;
import java.time.ZoneId; @NoArgsConstructor
@Data
public class PlayList {
private String uuid;
/***
* 播放时长
*/
private Float duration; /***
* 播放列表内容
*/
private String context; @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8")
private LocalDateTime createTime = LocalDateTime.now(ZoneId.of("+8")); private PlayList(Builder builder) {
setUuid(builder.uuid);
setDuration(builder.duration);
setContext(builder.context);
setCreateTime(builder.createTime);
} public static final class Builder {
private String uuid;
private Float duration;
private String context;
private LocalDateTime createTime = LocalDateTime.now(ZoneId.of("+8")); public Builder() {
} public Builder uuid(String uuid) {
this.uuid = uuid;
return this;
} public Builder duration(Float duration) {
this.duration = duration;
return this;
} public Builder context(String context) {
this.context = context;
return this;
} public Builder createTime(LocalDateTime createTime) {
this.createTime = createTime;
return this;
} public PlayList build() {
return new PlayList(this);
}
}
}
    /**
* 缓存Map
*
* @param key
* @param dataMap
* @return
*/
@Override
public <T> HashOperations<String, String, T> setCacheMap(String key, Map<String, T> dataMap) { HashOperations hashOperations = redisTemplate.opsForHash();
if (null != dataMap) {
for (Map.Entry<String, T> entry : dataMap.entrySet()) {
String hashKey = entry.getKey();
if(hashKey !=null){
hashOperations.put(key, hashKey, entry.getValue());
}
else {
log.error("出错了:{},hash键为null@{}",entry.getValue());
}
}
}
return hashOperations;
}
    @Override
public Boolean expire(String key, long timeout, TimeUnit unit) {
return redisTemplate.expire(key, timeout, unit);
}
import lombok.Data;
import lombok.NoArgsConstructor;
import net.bramp.ffmpeg.probe.FFmpegStream;
import org.apache.ibatis.type.JdbcType;
import tk.mybatis.mapper.annotation.ColumnType; import javax.persistence.Table;
import java.util.List; @Data
@NoArgsConstructor
@Table(name = "open_media")
public class AudioMediaFile extends MediaFile { /***
* 流通道数
*/
@ColumnType(column = "nb_streams",jdbcType = JdbcType.TINYINT)
byte nbStreams; byte nbPrograms; Integer startTime;
/***
* 格式名称
*/
String formatName; /***
* 多媒体播放时长
*/
Float duration; /***
* 比特率
*/
Integer bitRate; @ColumnType(column = "probe_score",jdbcType = JdbcType.TINYINT)
byte probeScore; /***
* 文件类型
*/
@ColumnType(column = "type",jdbcType = JdbcType.TINYINT)
byte type; List<FFmpegStream> streams; String metadata; private AudioMediaFile(Builder builder) {
setUuid(builder.uuid);
setName(builder.name);
setData(builder.data);
setMimeType(builder.mimeType);
setStamp(builder.stamp);
setSize(builder.size);
setNbStreams(builder.nbStreams);
setFormatName(builder.formatName);
setDuration(builder.duration);
setBitRate(builder.bitRate);
setProbeScore(builder.probeScore);
setType(builder.type);
setStreams(builder.streams);
setMetadata(builder.metadata);
} public static final class Builder {
private String uuid;
private String name;
private byte[] data;
private String mimeType;
private Long stamp;
private Long size;
private byte nbStreams;
private String formatName;
private Float duration;
private Integer bitRate;
private byte probeScore;
private byte type;
private List<FFmpegStream> streams;
private String metadata; public Builder() {
} public Builder uuid(String uuid) {
this.uuid = uuid;
return this;
} public Builder name(String name) {
this.name = name;
return this;
} public Builder data(byte[] data) {
this.data = data;
return this;
} public Builder mimeType(String mimeType) {
this.mimeType = mimeType;
return this;
} public Builder stamp(Long stamp) {
this.stamp = stamp;
return this;
} public Builder size(Long size) {
this.size = size;
return this;
} public Builder nbStreams(byte nbStreams) {
this.nbStreams = nbStreams;
return this;
} public Builder formatName(String formatName) {
this.formatName = formatName;
return this;
} public Builder duration(Float duration) {
this.duration = duration;
return this;
} public Builder bitRate(Integer bitRate) {
this.bitRate = bitRate;
return this;
} public Builder probeScore(byte probeScore) {
this.probeScore = probeScore;
return this;
} public Builder type(byte type) {
this.type = type;
return this;
} public Builder streams(List<FFmpegStream> streams) {
this.streams = streams;
return this;
} public Builder metadata(String metadata) {
this.metadata = metadata;
return this;
} public AudioMediaFile build() {
return new AudioMediaFile(this);
}
}
}
import lombok.Data;
import lombok.NoArgsConstructor; import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table; /***
* 多媒体文件
*/
@Data
@NoArgsConstructor
@Table(name = "open_media")
public class MediaFile { /**
* 音频文件
*/
public static final byte TYPE_AUDIO = 0x1; public static final byte TYPE_VIDEO = 0x2; public static final byte TYPE_DATA1 = 0x4; public static final byte TYPE_DATA2 = 0x8; /***
* 文件唯一标识
*/
@Id
@GeneratedValue(generator = "JDBC")
String uuid; /****
* 文件名
*/
String name; /***
* 解析后的数据流
*/
byte[] data;
/***
* 多媒体文件类型
*/
String mimeType; /***
* 创建文件的时间
*/
Long stamp; /***
* 文件大小
*/
Long size; private MediaFile(Builder builder) {
setUuid(builder.uuid);
setName(builder.name);
setData(builder.data);
setMimeType(builder.mimeType);
setStamp(builder.stamp);
setSize(builder.size);
} public static final class Builder {
private String uuid;
private String name;
private byte[] data;
private String mimeType;
private Long stamp;
private Long size; public Builder() {
} public Builder uuid(String uuid) {
this.uuid = uuid;
return this;
} public Builder name(String name) {
this.name = name;
return this;
} public Builder data(byte[] data) {
this.data = data;
return this;
} public Builder mimeType(String mimeType) {
this.mimeType = mimeType;
return this;
} public Builder stamp(Long stamp) {
this.stamp = stamp;
return this;
} public Builder size(Long size) {
this.size = size;
return this;
} public MediaFile build() {
return new MediaFile(this);
}
}
}
    static String toJson(Object value) throws JsonProcessingException {
ObjectMapper objectMapper =new ObjectMapper();
//属性值为null不输出
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
//默认值的不输出
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_DEFAULT);
//反斜杠转义其他字符
objectMapper.configure(JsonParser.Feature.ALLOW_BACKSLASH_ESCAPING_ANY_CHARACTER,true);
//所有键值用字符串形式包装起来
objectMapper.configure(JsonGenerator.Feature.WRITE_NUMBERS_AS_STRINGS,true);
return objectMapper.writeValueAsString(value);
}
import xxx.bean.AudioMediaFile;
import xxx.bean.MediaFile;
import xxx.bean.PlayList;
import xxx.bean.TransportSegment;
import xxx.service.MediaService;
import lombok.extern.slf4j.Slf4j;
import net.bramp.ffmpeg.FFmpeg;
import net.bramp.ffmpeg.FFmpegExecutor;
import net.bramp.ffmpeg.FFmpegUtils;
import net.bramp.ffmpeg.FFprobe;
import net.bramp.ffmpeg.builder.FFmpegBuilder;
import net.bramp.ffmpeg.job.FFmpegJob;
import net.bramp.ffmpeg.probe.FFmpegProbeResult;
import net.bramp.ffmpeg.probe.FFmpegStream;
import net.bramp.ffmpeg.progress.Progress;
import net.bramp.ffmpeg.progress.ProgressListener;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.tuple.ImmutablePair; import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors; /***
* 文件切割线程任务
* divides it into a series of small media segments of equal duration.
* @author dqk
*/
@Deprecated
@Slf4j
public class DivideTask implements Callable<ImmutablePair<PlayList, List<TransportSegment>>>
{ final Locale locale = Locale.US;
final FFmpeg ffmpeg = new FFmpeg();
final FFprobe ffprobe = new FFprobe(); ImmutablePair<FFmpegProbeResult, AudioMediaFile> pair; String filename;
String uuid;
byte[] data; public DivideTask(ImmutablePair<FFmpegProbeResult,AudioMediaFile> pair) throws IOException {
this.pair = pair;
} public DivideTask(String filename,String uuid,byte[] data) throws IOException {
this.filename = filename;
this.uuid = uuid;
this.data = data; //获取反序列化后文件的元数据信息
FFmpegProbeResult probeResult = ffprobe.probe(filename); long timestamp = LocalDateTime.now(ZoneId.of("UTC+8")).toInstant(ZoneOffset.ofHours(8)).toEpochMilli(); String metadata = MediaService.toJson(probeResult); AudioMediaFile.Builder builder = new AudioMediaFile.Builder()
.name(filename)
.uuid(uuid)
.streams(probeResult.streams)
.mimeType(probeResult.format.format_long_name)
.type(MediaFile.TYPE_AUDIO)
.stamp(timestamp)
.bitRate(Long.valueOf(probeResult.format.bit_rate).intValue())
.duration(Double.valueOf(probeResult.format.duration).floatValue())
.formatName(probeResult.format.format_name)
.nbStreams((byte) probeResult.format.nb_streams)
.size(probeResult.format.size)
.probeScore((byte) probeResult.format.probe_score)
.mimeType(probeResult.format.format_long_name)
.data(data)
.metadata(metadata);
this.pair = new ImmutablePair<>(probeResult,builder.build());
} public static String getString(InputStream stream) throws IOException {
return IOUtils.toString(stream,"UTF-8");
} @Override
public ImmutablePair<PlayList,List<TransportSegment>> call() throws Exception { FFmpegExecutor executor = new FFmpegExecutor(ffmpeg, ffprobe); final FFmpegProbeResult probe = pair.getLeft();
AudioMediaFile audioFile = pair.getRight(); final List<FFmpegStream> streams = probe.getStreams().stream().filter(fFmpegStream -> fFmpegStream.codec_type!=null).collect(Collectors.toList()); final Optional<FFmpegStream> audioStream = streams.stream().filter(fFmpegStream -> FFmpegStream.CodecType.AUDIO.equals(fFmpegStream.codec_type)).findFirst(); if(!audioStream.isPresent())
{
log.error("未发现音频流");
} String filename = probe.format.filename; Path nioFile = Paths.get(filename); String directory = nioFile.getParent().toString(); String uuid = audioFile.getUuid(); String output = String.format("%s%sstream.m3u8",directory, File.separator); FFmpegBuilder builder = new FFmpegBuilder()
.setInput(filename)
.overrideOutputFiles(true)
.addOutput(output)
.setFormat("wav")
.setAudioBitRate(audioStream.isPresent()?audioStream.get().bit_rate:0)
.setAudioChannels(1)
.setAudioCodec("aac") // using the aac codec
.setAudioSampleRate(audioStream.get().sample_rate)
.setAudioBitRate(audioStream.get().bit_rate)
.setStrict(FFmpegBuilder.Strict.STRICT)
.setFormat("hls")
.addExtraArgs("-hls_wrap", "0", "-hls_time", "5", "-hls_list_size","0")
.done(); FFmpegJob job =
executor.createJob(
builder,
new ProgressListener() { // Using the FFmpegProbeResult determine the duration of the input
final double duration_ns = probe.getFormat().duration * TimeUnit.SECONDS.toNanos(1); @Override
public void progress(Progress progress) {
double percentage = progress.out_time_ns / duration_ns; // Print out interesting information about the progress
String consoleLog = String.format(
locale,
"[%.0f%%] status:%s frame:%d time:%s fps:%.0f speed:%.2fx",
percentage * 100,
progress.status,
progress.frame,
FFmpegUtils.toTimecode(progress.out_time_ns, TimeUnit.NANOSECONDS),
progress.fps.doubleValue(),
progress.speed);
log.debug(consoleLog);
}
}); job.run(); if (job.getState() == FFmpegJob.State.FINISHED) { //排除的文件
String[] excludes = new String[]{
"wav","m3u8"
}; List<TransportSegment> segments = Files.list(Paths.get(directory)).filter(
path -> {
String extension = getFileExtension(path.getFileName().toString());
return !Arrays.asList(excludes).contains(extension);
}
).map(path -> {
String name = path.getFileName().toString();
try {
byte[] bytes = IOUtils.toByteArray(path.toUri());
TransportSegment segment = new TransportSegment
.Builder()
.bytes(bytes)
.filename(name)
.uuid(uuid)
.build();
return segment;
} catch (IOException e) {
log.error("读取文件失败:{}",e);
}
return null;
}).collect(Collectors.toList()); String context = getString(new FileInputStream(output));
PlayList playList = new PlayList.Builder()
.context(context)
.uuid(uuid)
.duration(Double.valueOf(probe.format.duration).floatValue())
.build(); return new ImmutablePair<>(playList,segments); }else {
log.error("文件分割发生不可预料的错误:{}");
} return null;
} private static String getFileExtension(String fileName) {
if (fileName.lastIndexOf(".") != -1 && fileName.lastIndexOf(".") != 0) {
return fileName.substring(fileName.lastIndexOf(".") + 1);
} else {
return "";
}
} }

最终生成结果

前端代码:

        var ctlVolume =$("#volume");
//音量
var level = ctlVolume.attr("min")/ctlVolume.attr("max"); var player = videojs('example-video');
// player.ready(function() {
// var _this = this
// //速率
// var playbackRate = $("#playbackRate").val();
// var speed = parseFloat(playbackRate);
//
// var volume = parseFloat($("#volume").val()/100.0);
//
// setTimeout(function() {
// _this.playbackRate(speed);
// _this.volume(volume);
// },20);
// }); var data = response.data;
var message = '消息:'+response.message+",code:"+response.code+",meta:"+JSON.stringify(data);
console.info(message); player.src('/media/'+data.uuid+'.m3u8');
player.play();
@RequestMapping(value = "{uuid}.m3u8")
public ResponseEntity<StreamingResponseBody> m3u8Generator(@PathVariable("uuid") String uuid){ String key = "media.".concat(uuid);
Map<String, Object> cached = cacheService.getCacheMap(key);
if(CollectionUtils.isEmpty(cached))
{
return new ResponseEntity(null, HttpStatus.OK);
}
String playlist = (String) cached.get("playlist");
String[] lines = playlist.split("\n"); //人为在每个MPEG-2 transport stream文件前面加上一个地址前缀
StringBuffer buffer = new StringBuffer(); StreamingResponseBody responseBody = new StreamingResponseBody() {
@Override
public void writeTo (OutputStream out) throws IOException {
for(int i = 0; i < lines.length; i++)
{
String line = lines[i]; if(line.endsWith(".ts"))
{
buffer.append("/streaming/");
buffer.append(uuid);
buffer.append("/");
buffer.append(line);
}else {
buffer.append(line);
}
buffer.append("\r\n");
}
out.write(buffer.toString().getBytes());
out.flush();
}
}; return new ResponseEntity(responseBody, HttpStatus.OK);
}

2020-1-7日更新

方法补充:getCacheService().setCacheMap(mediaId, mapping);

    /**
* 缓存Map
*
* @param key
* @param dataMap
* @return
*/
<T> HashOperations<String, String, T> setCacheMap(String key, Map<String, T> dataMap); /**
* 缓存Map
*
* @param key
* @param dataMap
* @return
*/
@Override
public <T> HashOperations<String, String, T> setCacheMap(String key, Map<String, T> dataMap) { HashOperations hashOperations = redisTemplate.opsForHash();
if (null != dataMap) {
hashOperations.putAll(key, dataMap);
}
return hashOperations;
}

saveSegments方法

  <insert id="saveSegments">
INSERT INTO open_segment(
uuid
,filename
,bytes
,create_time
)
VALUES
<foreach collection="list" item="item" index="index" separator=",">
(
#{item.uuid}
,#{item.filename}
,#{item.bytes}
,#{item.createTime}
)
</foreach>
</insert>

savePlayList方法

<insert id="savePlayList" useGeneratedKeys="true" keyProperty="id">
insert into open_playlist(uuid, duration, context, create_time)
values (#{uuid}, #{duration}, #{context}, #{createTime})
</insert>

java使用ffmpeg生成HLS切片文件的更多相关文章

  1. Java程序如何生成Jar 执行文件(2)

    一.用Eclipse生产Jar文件 注意:此方法可以打包含有第三方jar包的项目 1. 首先,右键你的Java工程,选择Export,在Java文件夹下选择Runnable JAR file,如下图所 ...

  2. Java程序如何生成Jar 执行文件(1)

    一.用Eclipse生产Jar文件 注意:此方法只能打包简单程序,不包含含有第三方jar包的项目 首先,看一下我的项目的目录结构: 1,项目名字上面点右键,选择Export,在选择java\JAR f ...

  3. java使用jdom生成xml格式文件

    本文生成xml使用的工具是jdom.jar,下载地址如下: 链接:https://eyun.baidu.com/s/3slyHgnj 密码:0TXF 生成之后的文档格式类型,就如上面的图片一样,简单吧 ...

  4. 转:如何利用已有的切片文件生成TPK

    Tpk是ArcGIS 10.1即将推出的一种新的数据文件类型,主要是用于将切片文件打包形成离线地图包.Tpk可以在ArcGIS Runtime中作为切片底图被加载.在ArcGIS 10.1中Tpk的生 ...

  5. 使用ffmpeg搭建HLS直播系统

    [时间:2018-04] [状态:Open] [关键词:流媒体,stream,HLS, ffmpeg,live,直播,点播, nginx, ssegment] 0 引言 本文作为HLS综述的后续文章. ...

  6. Java使用FFmpeg处理视频文件指南

    Java使用FFmpeg处理视频文件指南 本文主要讲述如何使用Java + FFmpeg实现对视频文件的信息提取.码率压缩.分辨率转换等功能: 之前在网上浏览了一大圈Java使用FFmpeg处理音视频 ...

  7. 10.nginx+ffmpeg上搭建HLS切片

    1.首先介绍一下HLS协议: (1)简介 这个协议是由苹果公司提出并推广使用的,维基百科介绍如下: HTTP Live Streaming(缩写是HLS)是一个由苹果公司提出的基于HTTP的流媒体网络 ...

  8. Java使用FFmpeg处理视频文件的方法教程

    这篇文章主要给大家介绍了关于Java使用FFmpeg处理视频文件的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面来一起学习学习吧 前言 本文主要 ...

  9. java中如何生成可执行的jar文件

    java中如何生成可执行的jar文件 最简单的方法就是: jar -cfe Card.jar CardLayoutDemo CardLayoutDemo$1.class CardLayoutDemo$ ...

随机推荐

  1. bug的编写技巧与级别划分

    一.bug编写技巧 确.清晰.简洁.完整.一致 二.bug包含的要素 缺陷ID.缺陷标题.测试环境.缺陷发现日期时间.缺陷提交人 缺陷优先级.缺陷严重等级.发现缺陷软件版本.测试类型 缺陷复现步骤.期 ...

  2. Linux secureCRT 介绍和安装和优化

    修改背景颜色

  3. 【安卓周记】笔记复习记录:No.2

    [安卓] 1. Activity横竖屏切换生命周期变化,分三种情况: 1. 没有配置android:configChanges:每次横竖屏切换都会重新走一遍生命周期 2. 配置android:conf ...

  4. 洛谷 P1439 【模板】最长公共子序列 题解

    每日一题 day40 打卡 Analysis 因为两个序列都是1~n 的全排列,那么两个序列元素互异且相同,也就是说只是位置不同罢了,那么我们通过一个book数组将A序列的数字在B序列中的位置表示出来 ...

  5. 一.使用LDAP认证

    作用:网络用户认证,用户集中管理 网络用户信息:LDAP服务器提供 本地用户信息:/etc/passwd /etc/shadow提供     LDAP服务器:虚拟机classroom     LDAP ...

  6. redis堵死致数据清空

    情景: zy的链路监控突然都恢复,而且在哪个时间段zabbix中显示回复,也发送了告警,但是实际上告警并没有发出来.这是不可能的情况,应该是redis缓存中的数据都被清空了,没有认为干预,需解决问题 ...

  7. Ubuntu 系统安装ssh的命令

    更新源列表 打开"终端窗口",输入"sudo apt-get update"-->回车-->"输入当前登录用户的管理员密码"-- ...

  8. P1902 刺杀大使

    题目描述 伊朗伊斯兰革命卫队(某恐怖组织)正在策划一起刺杀行动,他们的目标是沙特驻美大 使朱拜尔.他们来到了沙特驻美使馆,准备完成此次刺杀,要进入使馆首先必须通过使馆前 的防御迷阵. 迷阵由 n*m ...

  9. 复旦高等代数 I(15级)每周一题

    [问题2015A01]  证明: 第三类分块初等变换是若干个第三类初等变换的复合. 特别地, 第三类分块初等变换不改变行列式的值. [问题2015A02]  设 $n\,(n\geq 2)$ 阶方阵 ...

  10. 原创:C++实现的可排序的双向链表

    学习C++有一周了,今天用C++设计了一个双向链表,这个链表有排序功能,默认按升序排列,接受的参数可以是数字,也可以是字符串.现在把自己写的代码,分享出来.如果链表中接受的对象为Lexeme,可以用于 ...